非同期データを三つの状態で描く
サーバーや DB から届くデータには loading・data・error の三状態がある。Dart の Future/async/await を土台に、Riverpod の AsyncValue と FutureProvider で三状態をひとつの型として扱い、UI へ落とし込む。
前回、共有状態を Provider として持ち出し、watch / read / listen で読む方法を見ました。
ただし現実の状態は、最初からそこにあるとは限りません。サーバーや DB から非同期にやってくる
のがふつうです。
非同期データを扱うとき、初学者がまず取りこぼすのが「待っている間」と「失敗したとき」です。
成功して届いたデータ(data)だけを考えて UI を書くと、読み込み中は真っ白、失敗すると例外で
クラッシュ——になります。今回は、Dart の Future を土台に、loading / data / error の三状態を
AsyncValue と FutureProvider でひとつの型として扱い、UI に落とし込みます。
§ 01FUTURE非同期の基本
Future<T> は、「いまはまだ無いが、いずれ手に入る T」を表します。async を付けた関数の中で
await すると、その Future が完了するまでその関数の続きが待たされ、完了したら値が返ります。
失敗した場合は、await の地点で例外として飛んできます。
Future<List<Item>> fetchItems() async {final res = await http.get(Uri.parse('$base/items'));if (res.statusCode != 200) {throw ItemException('取得に失敗しました (${res.statusCode})');}return parseItems(res.body);}
この関数は「List<Item> をいずれ返す」という約束(Future)です。呼ぶ側は、返ってくるまで
待つ必要があります。この「待ち」を UI にどう見せるかが本題です。
§ 02THREE-STATESloading / data / error
非同期の取得には、必ず三つの状態があります。
- loading … 待っている(スピナーを出したい)
- data … 届いた(中身を描きたい)
- error … 失敗した(メッセージを出したい)
素朴に書くと、bool isLoading と List<Item>? data と Object? error の三つの変数を自分で
管理することになります。これらは組み合わせに意味の定義がなく(error と data が同時に
残っているとき何を出すのか、など)、状態の整合を自分で背負うことになって、setState の取り回しが
一気に煩雑になります。
§ 03ASYNCVALUE三状態をひとつの型で持つ
Riverpod の AsyncValue<T> は、この三状態をひとつの値にまとめた型です。中身は次の三つの
いずれかになります。
AsyncLoading<T>… 読み込み中AsyncData<T>… 値ありAsyncError<T>… 失敗(エラーとスタックトレースを保持)
三つの変数を自分で同期させる代わりに、AsyncValue ひとつが「いまどの状態か」を常に正しく表す——
これが要点です。
§ 04FUTUREPROVIDER非同期の値を宣言する
AsyncValue を自分で組み立てる必要はありません。FutureProvider に非同期関数を渡すと、Riverpod
がそれを実行し、結果を AsyncValue<T> として公開します。
final itemsProvider = FutureProvider<List<Item>>((ref) async {return fetchItems(); // Future を返す非同期関数});
itemsProvider を watch すると、返ってくるのは List<Item> ではなく AsyncValue<List<Item>>
です。実行開始時は AsyncLoading、成功すれば AsyncData、fetchItems() が投げた例外は
自動的に AsyncError に包まれます1await は失敗した Future を例外に変える。FutureProvider はその例外を捕まえて AsyncError に包むため、try/catch を UI 側に書かなくても error 状態として扱える。。三状態の管理を、Provider がまるごと引き受けてくれます。
§ 05WHENUI で三状態を描き分ける
受け取った AsyncValue は、.when() で三状態それぞれの描画を書き分けます。
class ItemsScreen extends ConsumerWidget {const ItemsScreen({super.key});@overrideWidget build(BuildContext context, WidgetRef ref) {final itemsAsync = ref.watch(itemsProvider);return itemsAsync.when(loading: () => const Center(child: CircularProgressIndicator()),error: (e, st) => Center(child: Text('エラー: $e')),data: (items) => ListView(children: [for (final it in items) ItemTile(item: it)],),);}}
三つの分岐を必ず全部書くことが、そのまま「loading と error を取りこぼさない」ことになります。
.when() は三状態の網羅をコードの形で強制してくれる、というわけです。
Dart 3 のパターンマッチで書くこともできます。ラベルなど一部だけ切り出したいときに便利です。
final label = switch (itemsAsync) {AsyncData(:final value) => '${value.length} 件',AsyncError(:final error) => 'エラー: $error',_ => '読み込み中…',};
なお、いったん取得したあとに再取得する場合、AsyncValue は前回の値を保持したまま次の loading
に入れます。「更新中も古い一覧を出しておく」といった挙動は、この保持を使って実現します2再取得中も AsyncValue は前回の値を value に保持する(Riverpod 3 で旧 valueOrNull は value に統合された)。手動の ref.refresh / invalidate による再取得では .when の skipLoadingOnRefresh が既定で true のため、更新中も古い表示が残る。依存変更による reload を制御するのは skipLoadingOnReload(既定 false)。。
§ 06SUMMARY非同期を型で捉える
Future<T>… いずれ手に入る値。awaitで待ち、失敗は例外で飛ぶ- 非同期の取得には loading / data / error の三状態が必ずある
AsyncValue<T>… 三状態をひとつの型にまとめる。変数を自分で同期させないFutureProvider… 非同期関数を渡すとAsyncValueとして公開。例外はAsyncErrorに自動で包む.when(loading/data/error)… 三状態を漏れなく描き分ける
これで「届くまで」を UI に表現できるようになりました。ただし非同期にはもう一つ、見落としがちな
落とし穴があります。待っている間に、その画面が閉じられているかもしれない——非同期の完了時、
Widget はもう存在しないことがあります。次回は短く、その context.mounted の話をします3取得だけでなく更新もする非同期状態には AsyncNotifier を使う。build() の戻り型は FutureOr<T> で、非同期初期化なら Future を返す。状態は同じく AsyncValue<T> として扱える。詳細は別の回で。。
- [1]
awaitは失敗したFutureを例外に変える。FutureProviderはその例外を捕まえてAsyncErrorに包むため、try/catchを UI 側に書かなくても error 状態として扱える。 ↩ - [2] 再取得中も
AsyncValueは前回の値をvalueに保持する(Riverpod 3 で旧valueOrNullはvalueに統合された)。手動のref.refresh/invalidateによる再取得では.whenのskipLoadingOnRefreshが既定でtrueのため、更新中も古い表示が残る。依存変更による reload を制御するのはskipLoadingOnReload(既定false)。 ↩ - [3] 取得だけでなく更新もする非同期状態には
AsyncNotifierを使う。build()の戻り型はFutureOr<T>で、非同期初期化なら Future を返す。状態は同じくAsyncValue<T>として扱える。詳細は別の回で。 ↩