※本記事は2021年9月2日に更新しました。
「FutureProviderで非同期で初期化できるけど、後から変更できないんだよな、、、」
「変更可能なProviderを非同期処理で初期化する方法ってないかな?」
例えばRiverpodとSQflite等のDB処理を併用しようとした時、
上記の問題に当たることが少なくないと思います。
本記事ではこの方法について解決方法が見つかりましたので、解説します。
前提としてFlutter 2.2.3,flutter_riverpod 1.0.0-dev.7 での環境下での回答となります。
(確認していませんが、flutter_hooksを使用した場合でも同様の考え方で解決できると思います。)
では、早速参りましょう!
問題点
まず、この話の何が問題なのかについて解説します。
Riverpodは状態管理に優れたパッケージです。
RiverpodにはProviderと呼ばれるものがあり、これを使って、状態管理を行います。
Providerには状態を後から変更できるもの(StateProvider等)や、
非同期での初期化処理に対応したもの(FutureProvider等)、
様々なProviderが用意されています。
しかし、非同期処理で初期化しつつ、状態を後から変更できるProviderは現在存在しません。
例えばデータベースからの値でProviderを初期化し、
ユーザーの入力に応じて状態を更新する、ということが直接的にはできないのです。
これに対応する方法を今回解説します。
解決方法
今回紹介する方法は、以下のissueの〜さんの回答となります。
その方法は以下の通りです。
- 後から変更可能なProviderを定義
- main関数内のrunAppの前で非同期処理を実行
- ProviderScopeのoverride内で2.の値を使いProviderの状態の初期化を実行
ただ、これだと、非同期処理の間何も表示されない画面でユーザーを待たせることになります。
これを解決するため、非同期処理の前に、待機画面を表示させるためのrunAppを追加します。
つまり以下のようになるわけです。
//********************変更可能なプロバイダーを定義********************
final countProvider = StateProvider((ref) => 0);
//************************************************************
void main() async {
//********************待機画面を表示するためのrunAppを追加********************
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
),
);
//************************************************************
//********************非同期処理で値を取得********************
int _count = await count();
//************************************************************
//********************ProviderScopeのoverridesでプロバイダーを再初期化********************
runApp(
ProviderScope(
overrides: [
countProvider.overrideWithProvider(StateProvider((ref) => _count)),
],
child: MyApp(),
),
);
//************************************************************
}
2回 runAppを行う、というかなり驚きの方法ですが、
確かにこれならユーザーを想定外の画面で待たせることはありません。
待機画面を作る必要がありますが、例のようにCircularProgressIndicator()で済ませるのが、
一番手っ取り早いかと思います。
以上が、変更可能なProviderを非同期処理で初期化する方法となります。
実際にこの方法を実装したサンプルアプリのコードを紹介します。
ちゃんと動くことが確認できるかと思います。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
//********************変更可能なプロバイダーを定義********************
final countProvider = StateProvider((ref) => 0);
//************************************************************
void main() async {
//********************待機画面を表示するためのrunAppを追加********************
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
),
);
//************************************************************
//********************非同期処理で値を取得********************
int _count = await count();
//************************************************************
//********************ProviderScopeのoverridesでプロバイダーを再初期化********************
runApp(
ProviderScope(
overrides: [
countProvider.overrideWithProvider(StateProvider((ref) => _count)),
],
child: MyApp(),
),
);
//************************************************************
}
Future<int> count() async {
int count = 0;
for (int i = 0; i < 1000000; i++) {
count++;
}
await Future.delayed(Duration(seconds: 10));
return count;
}
class MyApp extends ConsumerWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
title: 'Initialize by Asynchronous Processing',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Initialize by Asynchronous Processing'),
),
body: Center(child: Text('${ref.watch(countProvider).state}'))),
);
}
}
まとめ
今回は変更可能なProviderを非同期処理で初期化する方法となります。
データベースを使うアプリを作っていて、このことにかなり頭を悩ませました。
runAppを2回使うという離れ技にかなり驚いたのが、正直な感想です。
本記事が参考になれば幸いです。
コメント