8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FlutterAdvent Calendar 2024

Day 7

Riverpod で適切に非同期処理を扱う

Last updated at Posted at 2024-12-06

Riverpod における非同期処理て、複雑でむずかしくないですか?

例えば AsyncValue は、データ、ローディング、エラーという 3 つの状態を内包しているだけでなく、Riverpod v2 からは更に、リロードなど新しい概念も登場しました。

そんな AsyncValue を正しく扱えず、アプリケーションが意図した状態にならなかったり、思わぬ例外が throws され続け、オンコール対応に明け暮れたりと、苦労させられた経験があります。

今回は Riverpod でアプリケーションを開発・運用していく中で陥った罠と、そこから得られたベストプラクティスを紹介します。

value VS valueOrNull

どちらも以前に取得したデータを返します。しかし、以前に取得した値がない (エラー状態) の場合は、value は例外を throws してしまいます。

final anyFutureProvider = FutureProvider((ref) {
  // 何らかの例外が throws された
});

// value でアクセスしようとすると例外が throws される
ref.read(anyFutureProvider).value;

よって、value を使う場合は原則 hasValue によるチェックを行うか、もしくはエラー状態でないことが確信できる場合に限定して value を使うようにした方が良いでしょう。

// good
final any = ref.read(anyFutureProvider);

if (any.hasValue) {
  any.value;
}

もしくはよりセーフティな valueOrNull を使うことです。valueOrNull の場合は、ローディングやエラー状態のときは null を返してくれます。

// good
ref.read(anyFutureProvider).valueOrNull;

value (valueOrNull) の罠

前述のように、どちらも以前に取得したデータを返します。しかし、最新の状態を返す訳ではありません。

例えば、データの取得完了後、ローディング状態に変化した AsyncValue に対して value (valueOrNull) でアクセスすると、以前に取得したデータが返ってきます。

image.png

以前取得したデータがあるかどうかに関わらず、いま最新の状態を取得し、それに応じて UI を出し分けたいといった要件がある場合は unwrapPrevious() の使用を検討しましょう。

// good
ref.read(anyFutureProvider).unwrapPrevious()?.valueOrNull;

unwrapPrevious() は、以前に取得したデータの情報を持たない AsyncValue を返してくれます。

Future を await するときの罠

FutureProvider を ref.read (ref.watch) するときに、以下のようなコードを書くかもしれません。

await ref.read(anyFutureProvider.future);

これは一見正しいコードに見えますが、FutureProvider 内で例外が throws されると rethrows されてしまいます。try-catch で適切なエラーハンドリングを行うと良いでしょう。

// good
try {
  await ref.read(anyFutureProvider.future);
} catch (error) {
  // Do something.
}

AsyncNotifier における state 更新の罠

Notifier における state 更新と同様に、以下のようにして状態を更新していませんか?

state = AsyncDate('newValue');

これは間違ったコードではありませんが、以下のようなケースが起こり得ることは考慮しておくと良いでしょう。

  • 非同期処理が走っている間に代入で state 更新をしてしまうと、後から値が書きかわってしまい、意図した状態にならない可能性がある

image.png

update(:cb,onError:) 関数を使うと、非同期処理の完了を待ってから状態の更新を行うため、こういった事態を防ぐことができます。

image.png

update((previous) => "newValue");

ただし、状態がエラーの場合に update(:cb,onError:) を呼び出すと例外が throws されてしまうため、try-catch で囲むか、エラー時に呼び出される onError へコールバックを渡すようにしておきましょう。

// good
update(
  (previous) => "newValue",
  onError: (error, stackTrace) => "error",
);

さいごに

FutureProvider や AsyncNotifierProvider のライフサイクルは複雑です。今回紹介した tips はほんの一例です。

わたし自身現在も尚実践していますが、ProviderObserver を使って、Provider の生成や破棄の挙動を観察しながら、丁寧に開発を進めていくことをおすすめします。

ProviderScope(
  observers: [MyProviderObserver()],
  // ...
);

class MyProviderObserver extends ProviderObserver {
  @override
  void didDisposeProvider(ProviderBase<Object?> provider, ProviderContainer container) {
    print('${provider.runtimeType} was disposed');
  }

  // ...
}
8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?