😇

.family の 引数に自前のクラスを入れたら...[Flutter, Riverpod, freezed]

2021/10/27に公開

結論から

下記のドキュメントを見ればすぐに気づける。
https://riverpod.dev/docs/concepts/modifiers/family

.familyの引数に渡すことができるのは(渡すべきなのは)

  • プリミティブ型(bool/int/double/String)
  • 「==」「hashCode」を継承したimmutableオブジェクト

二つ目に関しては下記の記事など参考に自前で実装するか、
freezedパッケージで多機能なimmutableなクラスを生成するのが良さそう。
https://zenn.dev/iwaku/articles/2020-12-18-iwaku

ハマった流れとその解決策

.familyには1つしか値を渡せない

.familyには1つしか値を渡せない
ということで、自前で引数のクラスを作成することにしました。

class Args {
	Args({required this.hoge, required this.fuga});
	final String fuga;
	final String hoge;
}

こんな感じです。

単体なら普通に動く

最初の動作検証では普通に動き、特にエラーも出ませんでした。

final sampleStreamProvider = StreamProvider.autoDispose
    .family<bool, Args>((ref, args) {
    ...
});

...

ref.watch(sampleProvider)

...

複数になった時に謎現象が発生

今回の実装では、.familyをつけたStreamProviderを、別のProviderの中で、これまた別のStateNotifierProviderから取得した配列の繰り返しの中でIDをパラメータに渡して、watchする実装を行なっていました。

言葉だとわかりにくすぎるので、例を示します。

final likeCountStreamProvider = Provider.autoDispose
    .family<Map<String, int>, String>((ref, roomId) {
  final messages =
      ref.watch(roomProvider(roomId).select((value) => value.messages));
  final map = Map.fromEntries(messages.map((message) {
      final messageStream = ref.watch(sampleStreamProvider(
	      Args(hoge: roomId, fuga: message.id)
      ));
      
      return MapEntry(...)
});

...

ref.watch(likeCountStreamProvider)

...

謎ループが発生

ここで問題の無限ループが発生しました。
今回はStreamを扱っているため、どこかで予期せぬ再ビルドが発生し、それによってStreamに変更が加わり、無限ループを起こしていると、勝手に思い込みハマりました。

解決へ

最初の結論で書いたように、.familyのひきすうに良くない値(自前の超シンプルクラス)を入れたのが問題でした。

まず、ドキュメントで下記の記載を見つけ、「怪しい...」となり、

For families to work correctly, it is critical for the parameter passed to a provider to have a consistent hashCode and ==.

Ideally the parameter should either be a primitive (bool/int/double/String), a constant (providers), or an immutable object that overrides == and hashCode.

下記などを参考にしつつ、要件通り(「==」「hashCode」を継承したimmutableオブジェクト)のとしたら、無限ループはなくなり正常な動作が確認できました。
https://zenn.dev/iwaku/articles/2020-12-18-iwaku

ただ冗長になっている感が嫌だったので、この辺を自動でいい感じにしてくれる freezed でクラスを自動生成することにしました。

dartにおけるオブジェクトの基本を知っていれば想定できた問題な気がするのでハマる人は少ないかもですが、誰かの助けになれればと思い、書いてみました。

Discussion