Wiki_Flutter_Management_State - inoueshinichi/Wiki_Flutter GitHub Wiki

状態管理の基本

  • UIは「buildメソッドの戻り値」の結果によって決定され,
    「buildメソッドが状態を参照する」ことで,
    その時点でのUI構築結果を計算する.
    UIを動的に変化させたい場合は,
    状態を変更した上でbuildメソッドを呼び出しなおす(リビルドする)ことで実現する.

「宣言的」なUI構築

  • 「UI=f(State)」の考え方に従う.
  • UIは常にbuildメソッドの戻り値によってのみ決定する.
  • すべてのUIパーツに対する操作は一度「状態を表す値の更新」という処理に一本化され, その上でbuildメソッドにより更新後の値が参照され, 画面全体が最新のものになる.

メリット

  • どのような経緯があったとしても, 状態とbuildメソッドを確認すれば最終的に構築されるUIが明確になる.
  • UIを動的に変更するための経路が, 状態の変更とリビルドに一本化される.

状態管理手法

手法 公式/3rd 説明
StatefulWidget + setState()とコンストラクタ引数 公式 ReactのPropsと同じ考え方
InheritedWidget 公式 O(1)で子孫ウィジェットから参照する. dependOnInheritedWidgetOfExactType()とgetElementForInheritedWidgetOfExactType()
InheritedModel 公式 InheritedWidgetのサブクラス. InheritedWidgetによる変更を購読している子孫ウィジェットの中で, 一部のウィジットのみに変更を通知したい場合に使用する.
StreamBuilder 公式 BLoC (Business Logic of Component)の基礎. デフォルトで非同期処理に対応. StreamBuilderは, StatefulWidgetのサブクラス.
BLoC + InheritedWidget 公式 StreamBuilderによるBLoCとInheritedWidgetによるStateクラスへの高効率アクセス.
Redux 3rd 一箇所で状態管理. データの更新方向が一方通行.
Provider 公式認(3rd) InheritedWidgetによる状態管理手法をラッピング
BLoC + Provider 公式 BLoCとProviderの組み合わせ
Scoped Model 公式 GoogleのOS(Fuchsia)で利用されているパターン
Scoped Model + Provider 公式 -
Riverpot 3rd 状態管理手法を完全にFlutterの規則から切り離した独自の手法. React志向. 日本の開発だとこれが多い.

StatefulWidget + setState() + コンストラクタ引数 による状態管理

  • StatefulWidgetを継承したウィジェットクラスは不変なオブジェクト(Stateless)
  • 必ず1対1でStateオブジェクトを生成するメソッドを持つ.
  • buildメソッドはStateオブジェクトが担当する.
  • Stateオブジェクトは, 状態を持つ(可変)であるため任意のフィールドに任意の値を保持, 更新できる.
  • 状態更新時のリビルドを発生させるにはStateオブジェクトが持つsetStateメソッドを呼び出す.
  • setStateメソッドには, 状態を変化させる処理を持った関数オブジェクトを渡す.
<StatefulWidget> --- <State>・・・状態管理
                        ↑
                  <ChildWidget>
                        ↑
                  <AncestorWidget>・・・setState()

-------------------------------------------------
<StatefulWidget> --- <State>・・・状態管理
       ↑                ↑
  this.child       widget.child・・・setState()

コード例

// Widgetはいつ何時でも状態を持たない
class MyWidget extends StatefulWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  State<MyWidget> createState() => _MyState();
}

// Stateオブジェクトが状態を持つ
class _MyState extends State<MyWidget> {
  // ユーザーが入力した文字列(状態)
  var inputText = '';

  @override
  Widget build(BuildContext context) {
    run MaterialApp(
        ...
    );
  }
}

// 以下, ...のどこか

// 状態変化
TextField(
  onChanged: (text) {
    // 入力内容が変化するたびに呼び出される
    inputText = text;
  }
),

~~

// 状態更新
setState(() => {

});

InheritedWidgetによる状態管理

   <StatefulWidget> ---- <State>・・・状態管理
                            ↑ dependOnInheritedWidgetOfExactType() / getElementForInheritedWidgetOfExactType()
                    <InheritedWidget>
                       ↑         ↑
                     <UI_A>.     |
                                 |
                               <UI_*>

// StatefulWidgetクラスにInheritedWidgetクラス経由でStateクラスを参照する関数of()を実装する
e.g.
class MyStatefulWidget extends StatefulWidget {
  MyStatefulWidget({
    Key? key, 
    this.child,
  }) : super(key: key);

  @override
  MyState createState() => MyState();

  static MyState of(BuildContext context, {bool rebuild = true}) {
    if (rebuild) {
      MyInheritedwidget? inheritedWid = context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
      return inheritedWid!.data;
    }

    // リビルドされない
    InheritedElement? inheritedElem = const.getElementForInheritedWidgetOfExactType<MyInheritedWidget>();
    MyInheritedWidget inheritedWid = inheritedElem!.widget as MyInheritedWidget;
    return inheritedWid.data;

InheritedWidgetの取得方法

メソッドシンボル 計算時間 リビルド | 戻り値 利用可能タイミング
findAncestorWidgetOfExactType O(n) x Widgetのサブクラス
dependOnInheritedWidgetOfExactType O(1) ⚪︎ InheritedWidgetのサブクラス
getElementforInheritedWidgetOfExactType O(1) x InheritedElementクラス

findAncestorWidgetOfExactType

  • 祖先に当たるWidgetを検索
  • 計算時間O(n)
  • T? findAncestorWidgetOfExactType<T extends Widget>()

dependOnInheritedWidgetOfExactType

  • 祖先のInheritedWidgetを検索
  • 計算時間O(1)
  • T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>()

getElementForInheritedWidgetOfExactType

  • 祖先のInheritedWidgetに対応するElementを取得
  • 計算時間O(1)
  • Element<T extends InheritedWidget>? getElementForInheritedWidgetOfExactType()

InheritedModelによる状態管理

  • 文字列キー(aspect)を送信元のUIから送って, aspectに該当する子孫ウィジェットだけに更新を伝える.
  • フィルタリング関数of()が下記に変わる
// MyStatefulWidget
static MyState of(BuildContext context, String aspect) {
  return InheritedModel.inheritFrom<MyInheritedWidget>(context, aspect: aspect)!.data;

StreamBuilderによる状態管理(BLoC)

  • BLoCクラスとしてロジックをUIと分離する.
  • BLoCパターンは, そもそも非同期対応(Streamクラスを使う)なので,実装の手戻りが少ない.
  • BLoCパターンでは, setState()を呼ぶ必要がない.
  • BLoCでは, StreamControllerの*.sink.add()を用いて, streamに状態変数を書き込む.
  • BLoCでは, stream変数(Stream)を外部公開する.
  • BLoCでは, stream変数を更新する非同期関数を定義しておく.
  • UI側では, StreamBuilderを使う.
  • UI側では, 各Streamクラス毎にStreamBuilderのbuilder関数が呼ばれるだけなので高効率.

課題

  • 各WidgetにBLoCクラスをコンストラクタで渡している.
  • 解決策としては, InheritedWidget経由でBLoCインスタンスが存在するクラスにアクセスする.
// Process (Interface)
class CountRepository {
  Future<int> fetch() {
    return Future.delayed(const Duration(seconds: 1)).then((_) {
      return 1;
    });
  }
}

// BLoC
class CounterBloc {
  final CountRepository _repository;
  final _valueController = StreamController<int>();
  final _loadingController = StreamController<bool>();
  Stream<int> get value => _valueController.stream;
  Stream<bool> get isLoading => _loadingController.stream;
  int _counter = 0;

  CounterBloc(this._repository) {
    _valueController.sink.add(_counter);
    _loadingController.sink.add(false);
  }

  void incrementCounter() async {
    _loadingController.sink.add(true);
    var increaseCount = await _repository.fetch().whenComplete(() {
      _loadingController.sink.add(false);
    });
    _counter += increaseCount;
    _valueController.sink.add(_counter);
  }

  void dispose() {
    _valueController.close();
    _loadingController.close();
  }
}

~~~

// Blocクラスの初期化
@override
void initState() {
  super.initState();
  counterBloc = CounterBloc(CountRepository());
}

~~

// StreamBuilderの利用@受信側UI
@override
Widget build(BuildContext context) {
  return StreamBuilder<int>(
    //initialData: 0,
    stream: counterBloc.value, // Stream<int>
    builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
      return Text(
        "${snapshot.data}",
        style: Theme.of(context).textTheme.displayMedium,
      );
    }
  );
}

~~
// BLoCの更新メソッドの利用@送信側UI
return ElevatedButton(
  onPress:() {
    counterBloc.incrementCounter();
  },
  child: const Icon(Icons.add),
)

BLoC + InheritedWidgetによる状態管理

  • 登場人物
  1. BLoC(StreamController + 更新関数 + 状態変数)
  2. InheritedWidget
  3. StatefulWidget
  4. State
    <StatefulWidget> ----- <State>
        ↑ |- <Repo>           |- <BLoC>
        | |- child            |- widget.child
        | |- of()             |- widget.repo
        |
    <InheritedWidget>
     |             ↑
     ↓ 受信         | 送信
    <UI_A>        <UI_B>

Reduxによる状態管理手法

Providerによる状態管理手法

Riverpodによる状態管理手法

⚠️ **GitHub.com Fallback** ⚠️