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

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

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

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

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

## value VS valueOrNull

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

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

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

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

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

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

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

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

## value (valueOrNull) の罠

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230887/4080c74b-36a7-5c85-6070-4b52e3080e5e.png)

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

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

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

## Future を await するときの罠

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

```dart
await ref.read(anyFutureProvider.future);
```

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

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

## AsyncNotifier における state 更新の罠

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

```dart
state = AsyncDate('newValue');
```

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230887/30a3b52e-1b30-cc00-0b46-25234f6c9688.png)

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

https://pub.dev/documentation/riverpod/latest/riverpod/AsyncNotifier/update.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230887/8a8a7a91-5dba-6afb-caf8-409d0d949b95.png)


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

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

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

## さいごに

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

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

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

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

  // ...
}
```