Flutterで状態管理を始める 2020年版
11月 27日

なんぞや、私?

ということで Flutter Osaka 主催のミートアップにて、拙い司会を進めているのをしばしば見た方もいらっしゃると思います。本業でWebフロントエンド (今年は Vue/Nuxt.js よりも React/Next.js 多め) を中心にリプレースを進めている一方で、自身のキャリアが iOS developer (Objective-C/Swift) を源流にいまを生きていることから、最近は Flutter などのネイティヴアプリにも力を入れています。

直近の登壇

VR勉強会 3 (2020/06/21)

Slackを中心に世界は廻っている

PWA night 16 (2020/05/20)

もっと身近にやってた

Advent Calendar (Qiita) に参加させていただくこと自体初めてのことではありませんが、Flutterの記事を書くのは初めて。

今回簡易的な出退勤管理アプリの製作をひとつのネタに、状態管理を始める話をできれば ✍️

出退勤管理アプリの製作

ざっくりアプリ側に Flutter (製作開始当時 v1.1.0) を、続いてAPIサーバ側 AWS Lambda を採用しました。前者Flutterについては当時から「今後来る」ことを聞いており、一回やってみようという方針の下、また AWS Lambda については自身の得意分野の一つであるサーバレスが昂じて Node.js と合わせ採用している。

とはいえそこまで複雑な構成を敷いている訳ではありません。APIサーバで行っていることは、出勤 (もしくは退勤) した際に POST API を投げ、逐一DynamoDBにデータを置く。出勤、退勤それぞれを判別できるよう、アプリ側でStateを準備する。

  • 出勤の際はアプリ側で出勤用フラグを指定してAPIを投げる
  • 退勤の際はアプリ側で退勤用フラグを指定してAPIを投げる

こうしてアプリ側で出勤と退勤を区別する訳ですが、そのステータスは GET API を叩いて作られるもので ChangeNotifier という状態管理の手法を用いてアプリ内に格納します (後述)

APIサーバの内部実装については今回割愛させていただくとして、アプリ側に主眼をおきます。

http パッケージ を使いましょう。

final formatter = DateFormat("yyyyMMdd");
Map queryParams = {
  "date": formatter.format(new DateTime.now())
};
String queryString = Uri(queryParameters: queryParams).query;
final response = await http.get(url + '?' + queryString);

シンプルに FutureBuilder の下で、非同期にそのレスポンスを展開。通信の完了した状態と、完了していない状態によってウィジェットを分けることができる。

body: FutureBuilder(
  future: futureMethod,
  builder: (BuildContext context, AsyncSnapshot snapshot) {
    if (snapshot.hasData) {
      return Center(child: Text('Done'));
    } else if (snapshot.hasError) {
      return Center(child: Text('Error'));
    } else {
      return Center(child: CircularProgressIndicator());
    }
  },
),
Future futureMethod() {
  someFunc();
  return true;
}

ここで、この FutureBuilder のトリガーメソッド futureMethod の戻り型をboolに設定、完了したか否かを明確にすると良さそうです。

状態管理の手段に ChangeNotifer を使う

今回の出退勤管理アプリでは状態管理の手段に ChangeNotifier を使った。そもそも皆さんは状態管理の手段に何を使っていますか?

  1. Riverpod
  2. StateNotifer
  3. freezed

momoさんの書かれた Flutterの状態管理手法の選定 によるとざっくり上記三点に分けられると思う。今回の決め手としては一つに使われている場面が多く、事例と照らし合わせ易いこと。 blocパッケージの BlocProvider の内部実装にも Provider が利用されているなど親和性も高く、定番の状態管理ツールになっている気がしました。

あくまで今回私が ChangeNotifier を選択しその理由を上記に示したが、歴史的な背景や経緯など細かい情報についても Flutterの状態管理手法の選定 に分かり易く書かれており、ぜひチェックいただければと思います。

provider パッケージ を使いましょう。

実際に 以下 チェックいただくとすんなり入れられる。

前提として親ウィジェットで Provider を登録すること。

void main() => runApp(
  MultiProvider(
    providers: [
      ChangeNotifierProvider(create: (context) => StatusModel()),
    ],
    child: MyApp(),
  ),
  // MyApp()
);

この辺り React の Redux でも同じような実装を進めると思うので、私にとって導入のハードルはさほど高くありません。先に登録した Provider では、提供されたインスタンス StatusModel にアクセスするため、実際には Provider.of(context) のcontextに渡すことによって Consumer が有効化される仕組みで動作する。

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Consumer(builder: (context, model, child) {
      //
    }
  }
}

ステータス変更は Flutter Material のボタンをタップすることがトリガーとなるよう設定する訳ですが、ここでトリガーを実装する際 Provider.of(context, listen: false).changeStatus() のようにlistenをfalseにしたことで無駄な再レンダリングは行われない、すなわち必要なレンダリングのみで済ませることができる。

Widget build(BuildContext context) {
  return Scaffold(
    floatingActionButton: FloatingActionButton(
      onPressed: () => handleClick(),
      tooltip: '出勤する',
      child: const Icon(Icons.thumb),
    ),
  );
}
void handleClick() {
  //
}

そのステータスに応じて tooltip のテキストを変えたり、アイコンのタイプを変えたりできるようになります。あとは実際に handleClick で POST API を叩くよう設定することで、アプリ側からAPIサーバに対して出勤、退勤したことを知らせることができるようになる。

最後に、

このように出退勤管理アプリをひとつの例に、アプリ内でProviderの機能を使って状態管理をいかにして進めるかという焦点に合わせ書かせていただいた。

ただしこの他にも、ChangeNotifierと比べFlutter依存度の下がったかつProviderの機能を強力にした Riverpod に今、特に注目が集まっているようです。個人的にはFlutterバリバリと言えるプロジェクト経験がまだ無いので、一度そういうプロジェクトにジョインしてみたさも出てきました。現時点では Riverpod もまだ開発版であるため、実際の本番環境で使うのは来年までしばらく待った方が良いかもしれません (2021年は Riverpod元年 として来そうな予感を感じさせます)

という所感が出てきたところ、今日はこのへんで 👋

あわせてよみたい..