😸

Flutter StateNotifierProvider と 比較した Riverpod 雑感

2021/02/06に公開

実験的に部分的に入れ替えてみたので知見(と分かっていないことを)をメモ

=> スクラップにしてたものを記事化してみました。

三行まとめ

  • 確かによさそう
  • StateNotifier からはそんなに違和感なく置き換えできそう
  • 必ずしも同居しなくてよいという立場のようだけど、使いこなすには事実上 hooks は必要で、useXXX などについてある程度理解をしておく必要がある。useProvider, useEffect, useTextEditingController など...

StateNotifier のメソッド呼び出し

下記のとおり一括置換でも対応できるレベルの差分

FlatButton(
  onPressed: () {
  // StateNotifierProvider の場合、context.read<HogeNotifier>().hogeMethod()
  context.read(hogeNotifier).hogeMethod()
},
  child: Text(
    "Flat Button",
  ),
)

StateNotifier の state の参照

更新を検知しなくてよい場合・onPressed の中で利用する場合などは、context.read() でOK

FlatButton(
  onPressed: () {
    // StateNotifierProvider の場合、context.read<HogeState>().hogeMember
    final hoge = context.read(hogeNotifier.state).hogeMember
    print(hoge);  
},
  child: Text(
    "Flat Button",
  ),
)

StateNotifierProvider の場合、直接 HogeState(のインスタンス)を取り出していたが、StateNotifier(のインスタンス)を経由して触る必要がある模様。

更新を検知する場合、Consumer Widget を利用するか、useProvider を利用するかの2択。

context.select vs Consumer vs useProvider

vs と書いたが、

context.select<HogeState, String>((state) => state.hogeMember));

を代替するものが useProvider しかない模様。僕の理解では、Consumer は context.watch() を代替するもので、直接影響がない state が更新された場合でもリビルド対象になるので場合によってはUI描画パフォーマンスがよくない。

公式ドキュメントでは Reading a provider として Provider の参照について、Are you using flutter_hooks? という 2択がある。しかし実用上は、context.select() を使いたいか = StateNotifier の state の差分更新を検知してリビルド範囲を限定したいか の YES or NO に対し、NO(なWidget)なら Consumer を使ってもよいし、そうでなければ useProvider を使う、ということになり、useProvider は hooks の機能なので、flutter_hooks が結局必須になる。Consumer と useProvider は併用できるので併用もアリ?

StatelessWidget を ガシガシ HookWidget に書き換えていっていいのか、という点でためらっている人はいそう。

initState の置き換え

HookWidget にした場合(StatefullWidget ではないので) initState などのライフサイクルイベントと連動するメソッドはすべて使えない。StatefulHookWidget なるものもあるが用例を見かけない。

とあるWidgetが最初にビルドされたときに外部データを fetch する、みたいなことがやりたい場合、useEffect が利用できる模様。


void build(context) {
 useEffect(() {
    Future.microtask(() async {
      final result = await context.read(hogeNotifier).fetchData();
    });
    return; // dispose 処理がある場合ここで関数をもどす
  }, const []); // 多少のマジカル感があるが、こうしておくと リビルドが走っても一度だけの実行が保証される(と言い切っていいか?)
}

TextEditContoller の置き換え

TextField を扱うには TextEditController のインスタンス生成が必要で、関係して StateNotifier などに状態管理をまかせていても結局 StatefullWidget で定義する必要があった、のが、HookWidget にすることで


void build(context) {
  final _textController = useTextEditingController();
  TextField(
    autofocus: true,
    maxLength: 2000,
    controller: _textController,
    // ...
  );
}

などのように書ける。じーざす。これが一番感動的かも。公式では、useAnimationController の例が紹介されていて、得られる便益は同じ。

LocatorMixin について

LocatorMixin はない。ので、他の Notifier にアクセスする場合 ref.watch を使う。別の Notifier のメソッドを使いたい場合はコンストラクタで ref.read を引き渡す。

final oneStateNotifier =
    StateNotifierProvider.autoDispose((ref) => OneStateNotifier());

final twoStateNotifier =
    StateNotifierProvider.autoDispose((ref) {
  final notifier = TwoStateNotifier(ref.read);
  final removeListener = ref.watch(oneNotifier).addListener((state) {
    notifier.updatSomething(state.something)
  });
  ref.onDispose(removeListener);
  return notifier;
});

テストについて

Testing | Riverpod に詳しい。

StateNotifier に対するテストであれば、ProviderContainer を利用してカジュアルに書ける。StateNotifier が別の StateNotifier を (ref.watch などで)参照している場合、モッククラスを用意して差し替えるのが良さそう。

以上。

Discussion