🦦

Riverpod v2ではStreamProviderの扱いに気をつけよう

2023/02/03に公開

これは何?

https://github.com/rrousselGit/riverpod/issues/2121

GitHubのissueでバグっぽい挙動を報告したら想定通りだったという話です。

どういうこと?

業務で作ってるアプリのある部分で、StreamProviderを使うときに、データソースになる Stream が分岐するという状況がありまして。
例えばこんな感じ↓

final originalStream = StreamProvider.family.autoDispose<String, String>((ref, prefix) {
  return Stream.value('$prefix foo');
});

final prefixStore = StateProvider.autoDispose<String?>((_) => null);

final originalListener = StreamProvider.autoDispose<String>((ref) {
  final prefix = ref.watch(prefixStore);
  if (prefix == null) {
    return const Stream.empty();
  }
  
  return ref.watch(originalStream(prefix).stream);
});

issueに書いてある再現コードと同じですがw
このコード、要は引数を元に Stream を生成する originalStreamStateProvider に依存しながら呼び出すというコードなんですが、これを使ったときに Riverpod v1 系と v2 系で挙動が異なります。

こんな感じのコードを書いて検証してみました。

void main() async {
  final container = ProviderContainer();
  
  final value = await container.read(originalStream('#').future);
  print('first value: $value');
  
  container.listen(
    originalListener,
    (_, value) {
      print('$value');
    },
    fireImmediately: true,
  );
  
  container.read(prefixStore.notifier).state = '#';
  
}

v1 系ではデータソースが切り替わったタイミングで AsyncLoading を挟んだ後、前の Stream のデータを読み取ることが出来ます。

first value: # foo
AsyncLoading<String>()
AsyncData<String>(value: # foo)

対して v2 系では、データソースが切り替わると AsyncLoading の後は何か値が emit されない限り読み取ることが出来ません。

first value: # foo
AsyncLoading<String>()

v2 系の AsyncLoading は内部に値を持つことができる(UIのリフレッシュ中などに画面にデータを表示しながら AsyncLoading にできるという想定)ようになっていますが、このような状況だと内部に値も持っていません。

ではどうすれば?

StreamProvider を読み取ったときの値は AsyncValue なので、素直に AsyncValue を返すような Provider を作りましょう。

final originalListener = Provider.autoDispose<AsyncValue<String>>((ref) {
  final prefix = ref.watch(prefixStore);
  if (prefix == null) {
    return AsyncLoading();
  }
  
  return ref.watch(originalStream(prefix));
});

あるいは、AsyncLoadingAsyncError が伝わらなくてもいい(伝えたくない)場合は FutureProvider として StreamProvider.future を返すのも手です。

final originalListener = FutureProvider.autoDispose((ref) {
  final prefix = ref.watch(prefixStore);
  if (prefix == null) {
    return null;
  }
  
  return ref.watch(originalStream(prefix).future);
});
Sun* Developers

Discussion