くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Flutterの安全でリトライ可能な初期化フローの記事を読んでみた

この記事に出てきた話で、

「スプラッシュスクリーンで全て初期化するのではなく」
「読込中の画面を別途用意して、リトライ可能な初期化にしよう」
というのが、気になり、原文を読んでみたときの備忘録(*´ω`*)

いつもの初期化

いつもはこんな感じに、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でおこなったりいろいろ大変。。。

また、起動時にエラーが起こったときに、リトライできず、
アプリを閉じて、再起動してもらわないといけない
。。。

この記事のやり方

大まかな図がこれ。

How to Startup

  • 起動後、AppStartupWidetを表示(実質、自前のSplashScreenみたいなもの)
  • AppStartupWidet内で、初期化状態に応じて、表示するWidgetを切り分ける
    • 読み込み中: AppStartupLoadingWidgt(全画面のローディング画面など)
    • エラー時: AppStartupErrorWidgt(全画面のエラー画面など)
    • 初期化完了時: MainApp(正常終了したときの画面)

riverpodを使った初期化Provider

各種Widetを用意する前に、初期化処理や初期化状態を保持するProviderを用意する

Startup with 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(),
  ));
}

以上!! なるほど、確かに、これは便利(*´ω`*)

参考にしたサイト様