🐥

Flutterにおける状態管理入門

2024/10/03に公開

はじめに

自分自身はWebエンジニア出身でFlutterでの開発経験はほぼ皆無なのですが、個人的に最近Flutterの勉強を始めています。

Flutterの状態管理の手法について色々調べてみたので、まとめてみました。

そもそも状態とは

アプリケーションにおいて「状態」とは、ざっくり「変化しうるデータ」のことです。

SPAやモバイルアプリなどのフロントエンドの開発においては、フロントエンド側の多くの「状態」を保持し、「状態」に応じたUIに変化させる必要があります。

フロントエンドが保持する状態は様々で、

  • APIを通じて取得したサーバ側のデータ
  • ユーザーがフォームに入力したデータ
  • ボタンの選択状態、モーダルの表示有無などの一時的なデータ
    などが考えられます。

状態の分類

Flutterにおいて状態は以下の2種類に分けられます。

  • 単一のWidgetに閉じた状態: Ephemeral State
  • 複数のWidgetで共有する状態: App State

Ephemeral StateはFlutterが公式で仕組みを用意してくれていますが、
App Stateについては様々な方法が存在しているため、よくFlutter界隈で言われる状態管理は主に後者の状態をどう管理するか?を対象としています。

状態管理とはどういうことか

状態管理とは、状態の整合性が破綻しないように状態を「どこに保持するか」そして「どんなフローで状態を変更して検知させるか」といったことをルールを決めて扱うことです。

前述の通り、App Stateはルールが明確に決まっておらず、様々な手法が存在するため各組織のケイパビリティに合わせて選ぶ必要があります。

状態管理パターン

Flutterにおける状態管理のざっくり歴史

Flutterの状態管理のパターンはざっくり以下のような遷移を辿ってきています。

  • Stateful Widget+setState → ScopedModel → BLoC → Provider → Riverpod

ScopedModelとBLoCは現在主流ではなさそうだったので、今回の調査からは省きました。

Stateful Widget+setStateパターン

Flutterにおける状態管理で最もオーソドックスなのがこの Stateful Widget Pattern

親WidgetであるStateful WidgetにUI/State/Logicを全て持たせます。
子Widgetは全てStatelessWidgetで親のstateを更新するメソッドを叩くのみで、子Widgetにはコンストラクタの引数に渡したいメソッド等を渡していってバケツリレーを行います。

React/Vueでいうところのpropsのようなものです。Flutterはクラスベースなので、コンストラクタ経由で引数を渡していきます。

問題点

  1. 1画面で完結している画面なら、StatefulWidget+setStateパターンで何の問題もないですが、ツリー上で離れた位置にあるWidgetの間で、Widgetの状態を変更したりビジネスロジックを呼び出したりする時に問題が発生します

    • Widget間の連携をするには、何かしらグローバルに状態を管理できるものが必要になりますが、Stateful Widget+setStateだけではそれが実現できません。
  2. UI/State/Logicが同一に存在するので、テストが書きづらくテスタビリティが低下するのと、復数メンバーで実装する時のメンテナビリティが下がる

    • 状態を変更する処理やそれに伴うロジックを画面に書くと、コードがごちゃごちゃします

所感

画面が2〜3画面でstateが1画面で完結していて作業者も1人の個人アプリなら、古き良きStateful Widget Patternで問題はなさそう。開発者が今後増えることが予想されるなら、現実的ではないでしょう

Providerパターン

StatefulWidget+setStateのツリー上で離れた位置にあるWidgetの間で状態が共有できないという問題を解決するために生まれたパターン。

Provider自体は「値の変更を通知したり、検知したりすることができるもの」

改善されたポイント

  1. グローバルなStateを参照できるようになったので、StatefulWidget経由じゃなくても状態を共有できるようにした。変更用のメソッドを渡すことも可能

    • 完全にグローバルではなく、渡したいコンポーネントをProviderで囲むことによってその配下のWidgetにデータを渡すことができる
    • ReduxのContainerコンポーネント(ReduxのStore から値とdispatch関数を注入するもの)のようなものといえる。名前もProviderでReactからインスパイアを受けていそう。
    import { Provider } from 'react-redux'
    ...
    const AComponent = (props) => ( 
      <Provider store={store_created}>
         <AContainerComponent />
         ...
      </Provider>
    )
    
    • FlutterのProviderはいろんな種類があり、状態変更以外も検知できるらしい
      • 状態変更だけであれば、ChangeNotifierProviderというProviderを使う
  2. Providerを使うことで、状態の保持と変更処理をUIから分離することができる

    • Provider + StateNotifierというパターンにすると状態クラスに含まれていた更新ロジックも別で切り出すことができる

問題点

  • Providerはグローバルに定義はできない
    • 全てのWidgetで共有したい状態を持つことはできない
  • ProviderパターンではUIと状態に関してしか分離はできないので、メンテナンス性・テスタビリティ性には改善の余地が残る

所感

ProviderはFlutterの公式ライブラリなので公式にこだわるなら状態管理はProviderに寄せるというのもあり。ただRiverpodも今ではかなりメジャーなライブラリなので使ってみてもいいかもしれない。

Riverpodパターン

  • Providerと同じ作者のライブラリでProvierパッケージの上位互換。Providerでは難しい痒いユースケースに手が届くらしい。
  • flutter hooks推し
    • 公式がflutter hooksとの併用を推している。Flutterの公式は基本的にクラスでWidgetを定義していくスタイルなので、hooksが今後広がっていくかはまだ未知数
    • ReactにFlutterが今後インスパイアされて関数&hooksのスタイルになっていく可能性はあるかも
    • hooksを使うと色々楽でFlutterで辛い所を解消してくれる機能もあったりするらしいので、使ってみるのはあり
  • DI機能も提供してくれる
    • レイヤードアーキテクチャと組み合わせる時に便利そう

状態管理とアーキテクチャはまた別スコープ

Provider、Riverpodはあくまで状態管理が主なので、MVVM・レイヤードアーキテクチャといったアーキテクチャの話とは別スコープで捉えると覚える内容が少なくすんで入門者は理解しやすいと思います。

さいごに

Flutterの状態管理についてはProviderとRiverpodを使っておくのが一番無難そうだということが今回の調査でわかりました。

入門者はまずはRiverpodの使い方を覚えてFlutterにおける状態管理の全体感を把握することから始めるといいかもしれません。そして経験値が溜まってきてRiverpodを使うほどではないケースについてはProviderをカスタマイズして使ったり、自分なりのパターンを検討していくのが良さそうです。

Discussion