はじめに
サーバーと通信を行うアプリを想定して、エラーハンドリングを含めるとどのような形で実装するのが良さそうかサンプルコードを書こうとしています。
その過程で、FutureProvider
の動きについて疑問があったので調査してみました。
調査の内容ですが、再読み込み時にどのような内容に変化するのかということに焦点を当てています。
FutureProviderが再読み込みされるタイミングは下記の二つです(それ以外にもあったら教えてください)
・ref.invalidate実行時
・FutureProvider内でwatchしているプロパイダーが変更された時
この記事では上記の二つの動作でどのように状態が遷移するのかを確認しました。
コード
動作させるコードを以下に書いておきます。
final notificationsErrorProvider = StateProvider((ref) => false);
final notificationsProvider = FutureProvider<List<String>>(
(ref) async {
await Future.delayed(
const Duration(
seconds: 1,
),
);
// notificationsErrorProviderをwatchしているので、内容が変わったら再度実行されます。
if (ref.watch(notificationsErrorProvider)) {
// throw Exception("notifications error");
print("error");
}
return [
"notification1",
"notification2",
];
},
);
class ErrorHandlingPage extends ConsumerWidget {
const ErrorHandlingPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(notificationsProvider, (previous, next) {
print("previous isLoading: ${previous?.isLoading}");
print("previous isRefreshing: ${previous?.isRefreshing}");
print("previous isReloading: ${previous?.isReloading}");
print("next isLoading: ${next.isLoading}");
print("next isRefreshing: ${next.isRefreshing}");
print("next isReloading: ${next.isReloading}");
print("previous: $previous");
print("next: $next");
});
final notifications = ref.watch(notificationsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('error handling page'),
),
body: SafeArea(
child: Center(
child: notifications.isLoading
? const CircularProgressIndicator()
: Column(
children: [
ElevatedButton(
onPressed: () {
// ここで確認したい動作のコメントを外し、1か2を実行。
// 1:ref.read(notificationsErrorProvider.notifier).state =
// true;
// 2:ref.invalidate(notificationsProvider);
},
child: const Text("change"),
),
Expanded(
child: ListView.builder(
itemCount: notifications.value!.length,
itemBuilder: (context, index) {
return Text(notifications.value![index]);
},
),
)
],
),
),
),
);
}
}
確認結果
page.dart内の1の動作を「watchしているproviderを変更」、2の動作を「ref.invalidateを実行」と呼ぶことにします。
状態の変化結果
状態とは以下のメソッドのprevious
とnext
をコンソール出力したものです。
ref.listen(notificationsProvider, (previous, next) {})
watchしているproviderを変更
-
初回表示時
以下のようにAsyncLoading
からAsyncData
に変更
AsyncLoading<List<String>>()
→AsyncData<List<String>>(value: [notification1, notification2])
-
changeボタン押下時
以下のようにAsyncData
からAsyncLoading
に変更
(ローディング状態となる。valueは保持したまま。)
AsyncData<List<String>>(value: [notification1, notification2])
→AsyncLoading<List<String>>(value: [notification1, notification2])
-
FutureProvider完了後
以下のようにAsyncLoading
からAsyncData
に変更
AsyncLoading<List<String>>(value: [notification1, notification2])
→
AsyncData<List<String>>(value: [notification1, notification2])
ref.invalidateを実行
-
初回表示時
以下のようにAsyncLoading
からAsyncData
に変更
AsyncLoading<List<String>>()
→AsyncData<List<String>>(value: [notification1, notification2])
-
changeボタン押下時
以下のようにAsyncData
からAsyncData(isLoading: true)
に変更
AsyncData<List<String>>(value: [notification1, notification2])
→AsyncData<List<String>>(isLoading: true, value: [notification1, notification2])
-
FutureProvider完了後
以下のようにAsyncData(isLoading: true)
からAsyncData
に変更
AsyncData<List<String>>(isLoading: true, value: [notification1, notification2])
→AsyncData<List<String>>(value: [notification1, notification2])
違いについてまとめ
違いについてまとめると、以下の点がそれぞれ異なりました。
- watchしているproviderを変更した場合、AsyncDataがAsyncLoadingとなる
- ref.invalidateを実行した場合、AsyncDataでisLoadingがtrueとなる
以下の記事によるとAsyncLoadingは初回時のみ返ってくるみたいです。
そのことから、watchしているproviderを変更した場合は初期化されているともいえるのかな?
ただ、そう呼ぶと、ref.invalidateを実行した場合はなんと呼べばいいのかなんとも言えないです。
isLoading,isRefreshing,isReloadingの変化結果
watchしているproviderを変更
- 初回表示時
プロパティ名 | previous | next |
---|---|---|
isLoading | true | false |
isRefreshing | false | false |
isReloading | false | false |
- changeボタン押下時
プロパティ名 | previous | next |
---|---|---|
isLoading | false | true |
isRefreshing | false | false |
isReloading | false | true |
- FutureProvider完了後
プロパティ名 | previous | next |
---|---|---|
isLoading | true | false |
isRefreshing | false | false |
isReloading | true | false |
ref.invalidateを実行
- 初回表示時
プロパティ名 | previous | next |
---|---|---|
isLoading | true | false |
isRefreshing | false | false |
isReloading | false | false |
flutter: previous isLoading: false
flutter: previous isRefreshing: false
flutter: previous isReloading: false
flutter: next isLoading: true
flutter: next isRefreshing: true
flutter: next isReloading: false
- changeボタン押下時
プロパティ名 | previous | next |
---|---|---|
isLoading | false | true |
isRefreshing | false | true |
isReloading | false | false |
- FutureProvider完了後
プロパティ名 | previous | next |
---|---|---|
isLoading | true | false |
isRefreshing | true | false |
isReloading | false | false |
違いについてまとめ
違いについてまとめると、isRefreshingとisReloadingがそれぞれ異なりました。
- watchしているproviderを変更した場合、isReloadingがtrueとなる
- ref.invalidateを実行した場合、isRefreshingがtrueとなる
つまり、watchしているproviderを変更した場合はリロード、ref.invalidateを実行した場合はリフレッシュ扱いとなるようです。
さいごに
細かく挙動の違いを見てきましたが、ローディング状態を取得したいときは素直にisLoadingを使えば良さそうです。
watchしているproviderを変更された場合とref.invalidateを実行した場合で挙動を変えたい場合は、isRefreshingとisReloadingの参照に気をつける必要がありますね。