🐕

ConsumerWidgetとStatefulConsumerWidgetの使い分け

2024/04/01に公開

Summary

  • Widgetをまたぐ状態管理:ConsumerWidget
  • 単一のWidget内での状態管理:StatefulWidget
  • 両方使う場合:ConsumerStatefulWidget

状態管理は全部Providerでやるものだと思っていた私

RiverpodはWidgetをまたいで状態管理ができるんだ!すごい!
すごいからRiverpod(Provider)を使おう!やったね!
※RiverpodとProviderの関係性も曖昧だった私(自分の理解不足さよ…)

以前の私はそう思っていました。公式ドキュメントをよく読むんだ、昔の私よ。

プロバイダは、共有ビジネス状態のために設計されています。ローカル・ウィジェットの状態には使用されない:

フォーム状態の保存
現在選択されているアイテム
アニメーション
一般的にFlutterが "コントローラ"(例えばTextEditingController)を扱うものすべて

ローカルウィジェットの状態を処理する方法を探しているなら、代わりに flutter_hooks を使うことを検討してください。

Providers are designed to be for shared business state. They are not meant to be used for local widget state, such as for:

  • storing form state
  • currently selected item
  • animations
  • generally everything that Flutter deals with a "controller" (e.g. TextEditingController)

If you are looking for a way to handle local widget state, consider using flutter_hooks instead.

https://riverpod.dev/docs/essentials/do_dont#avoid-using-providers-for-local-widget-state

Providerはビジネスロジックに関する状態の共有を目的として設計されている。
なので、ローカルな状態(フォームの情報とか、選択されてるアイテムとか)に関してはProviderを用いるべきではない。
そういったローカルな状態に関してはHooksを使ったらいいし、Hooksのメリットが分からない(必要性を感じない)うちはStatefulWidgetを使うといい。

で、どっちも必要なケースにはConsumerStatefulWidgetを使おうねと。

仕組みをおさらいしておく

再学習する過程でStateってなんだっけ?状態のデータ(要素)の定義、そのデータを更新するロジックってどこに書かれてたっけ?となったのでほぼ自分用にメモです。

StatefulWidget

コードは公式から。
https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html

”State”の和訳が”状態”なものだから表現が難しいのだけど、Stateは状態の要素、状態更新のロジック、UIの定義の3つを含んでいる。StatefulWidgetはFlutter flameworkの窓口的な存在であって、StatefulWidget内では状態の生成のみ行っている。

// これがStatefulWidget
class Bird extends StatefulWidget {
  const Bird({
    super.key,
    this.color = const Color(0xFFFFE306),
    this.child,
  });

  // これは状態の要素ではない
  final Color color;
  final Widget? child;

  // createState()がFlutter flameworkから呼び出され、
  // 状態オブジェクトである_BirdState()が生成される
  
  State<Bird> createState() => _BirdState();
}


// これがState、Stateはbuild()メソッドを含みUIも定義する
class _BirdState extends State<Bird> {
  // 状態の要素を定義
  double _size = 1.0;

  // 要素ごとの更新ロジック
  void grow() {
    // setState()がトリガーとなってUIの再構築がされる
    setState(() { _size += 0.1; });
  }

  // initState()で状態の初期化処理を実装することもできる
  // @override
  // void initState() {
  //   super.initState();
    // なんらかの初期化処理
  // }

  // UI中で状態の情報を利用する
  
  Widget build(BuildContext context) {
    return Container(
      color: widget.color,
      transform: Matrix4.diagonal3Values(_size, _size, 1.0),
      child: widget.child,
    );
  }
}

Riverpod(ConsumerWidget)

こちらはRiverpodの公式から。
https://riverpod.dev/docs/introduction/getting_started

ConsumerWidgetはStatelessWidgetなので、MyAppクラスの内部で状態の定義だったり更新ロジックを持たない(状態に対して全く干渉しないわけではない、onTap()などをトリガーとして変更を加えることはする)。

状態は外部のグローバルな環境で定義し、その状態の更新ロジックもグローバルな環境で定義する。
詳しくはRiverpod公式にあるTodoアプリのサンプルコードを参照。

// 状態の要素、更新ロジックは外部で定義
// We create a "provider", which will store a value (here "Hello world").
// By using a provider, this allows us to mock/override the value exposed.

String helloWorld(HelloWorldRef ref) {
  return 'Hello world';
}

// ConsumerWidgetはStatelessWidget
// Extend ConsumerWidget instead of StatelessWidget, which is exposed by Riverpod
class MyApp extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // WidgetRefのオブジェクトrefを介してProviderにアクセス
    final String value = ref.watch(helloWorldProvider);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Center(
          child: Text(value),
        ),
      ),
    );
  }
}

Discussion