この記事に出てきた話で、
「スプラッシュスクリーンで全て初期化するのではなく」
「読込中の画面を別途用意して、リトライ可能な初期化にしよう」
というのが、気になり、原文を読んでみたときの備忘録(*´ω`*)
いつもの初期化
いつもはこんな感じに、main
内のrunApp
を呼び出す前にやる感じ
void main() async { try { // 何らかの初期化処理 await someAsyncCodeThatMayThrow(); // アプリの起動 runApp(const MaterialApp(home: MainApp())); } catch (e, st) { // ログ出したり、 log(e.toString(), stackTrace: st); // ErrorWidgetを表示したり runApp(const MaterialApp(home: AppStartupErrorWidget(e))); } }
ただこれだと、someAsyncCodeThatMayThrow()
でエラーが起こったときに、
ログの記録や適切なエラーWidgetの表示をcatch
でおこなったりいろいろ大変。。。
また、起動時にエラーが起こったときに、リトライできず、
アプリを閉じて、再起動してもらわないといけない。。。
この記事のやり方
大まかな図がこれ。
- 起動後、
AppStartupWidet
を表示(実質、自前のSplashScreenみたいなもの) AppStartupWidet
内で、初期化状態に応じて、表示するWidgetを切り分ける- 読み込み中:
AppStartupLoadingWidgt
(全画面のローディング画面など) - エラー時:
AppStartupErrorWidgt
(全画面のエラー画面など) - 初期化完了時:
MainApp
(正常終了したときの画面)
- 読み込み中:
riverpodを使った初期化Provider
各種Widetを用意する前に、初期化処理や初期化状態を保持するProviderを用意する
たとえば、起動時にSharedPreferenceを初期化するなどは、こんな感じ
// SharedPreferencesのProvider @Riverpod(keepAlive: true) Future<SharedPreferences> sharedPreferences(SharedPreferencesRef ref) => SharedPreferences.getInstance(); // 初期化処理をまとめたProvider @Riverpod(keepAlive: true) Future<void> appStartup(Ref ref) async { // sharedPreferencesの初期化: .featureをつけて、完了まで待つ await ref.watch(sharedPreferencesProvider.future); // dispose時の処理 ref.onDispose(() { ref.invalidate(sharedPreferencesProvider); }); }
AppStartupWidget
/// Widget class to manage asynchronous app initialization class AppStartupWidget extends ConsumerWidget { const AppStartupWidget({super.key, required this.onLoaded}); final WidgetBuilder onLoaded; @override Widget build(BuildContext context, WidgetRef ref) { // 初期化処理の状態を監視して final appStartupState = ref.watch(appStartupProvider); // 状態に応じて、Widgetを出し分ける感じ return appStartupState.when( // 読込中 loading: () => const AppStartupLoadingWidget(), // エラー発生時 error: (e, st) => AppStartupErrorWidget( message: e.toString(), // リトライは、appStartupProviderを破棄して、再度、初期化する onRetry: () => ref.invalidate(appStartupProvider), ), // 読み込みが完了したら、ログイン画面やログイン後の画面を表示 data: (_) => onLoaded(context), ); } }
main
はこんな感じ
void main() { runApp(const ProviderScope( child: MaterialApp( home: AppStartupWidget( onLoaded: () => MainApp(), ), ), )); }
go_routerを利用する場合
go_routerを使うときはこんな感じ
ディープリンク対応なども考えた場合、
AppStartupWidget
をgo_routerのrouteに含めるとややこしいので、
builderのchildをwrapする形で、表示の切り替えをおこなう
void main() { runApp(const ProviderScope( child: RootAppWidget(), )); } class RootAppWidget extends ConsumerWidget { const RootAppWidget({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final goRouter = ref.watch(goRouterProvider); return MaterialApp.router( routerConfig: goRouter, builder: (_, child) { // AppStartupWidgetでwrapし、 // go_routerのrouteには含めない return AppStartupWidget( onLoaded: (_) => child!, ); }, ..., ); } }
これでDeepLinksにも対応しつつ、
安全でリトライ可能な初期化ができるようになる
Tips / FAQ
依存関係がない場合は、並列で初期化する
依存関係がない場合は、Future.wait
を使って、
並列で初期化すると、起動時間が短くできる
@Riverpod(keepAlive: true) Future<void> appStartup(AppStartupRef ref) async { await Future.wait([ ref.watch(sharedPreferencesProvider.future), ref.watch(onboardingRepositoryProvider.future) ]); }
overrideWithValueとの使い分け
こんな感じのパターンもよく見かける
100%成功して、エラーが発生しないならこれでもOKとのこと
APIなどネットワークが必要な場合があるものは、
エラーやリトライできる用法がよいので、appStartupProvider
の方がよい
@Riverpod(keepAlive: true) SharedPreferences sharedPreferences(SharedPreferencesRef ref) => throw UnimplementedError(); void main() async { final sharedPreferences = await SharedPreferences.getInstance(); runApp(ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(sharedPreferences) ], child: const MainApp(), )); }
以上!! なるほど、確かに、これは便利(*´ω`*)