【Flutter】変更可能なProviderを非同期処理で初期化する方法【Riverpod】

Flutter基礎

※本記事は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の〜さんの回答となります。

Get an asynchronously initialized object to a Provider · Issue #329 · rrousselGit/river_pod
I would like to make the state managed by a StateNotifier be persistent and survive app restarts. Is there anything out of the box that I could use, so that whe...

その方法は以下の通りです。

  1. 後から変更可能なProviderを定義
  2. main関数内のrunAppの前で非同期処理を実行
  3. 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回使うという離れ技にかなり驚いたのが、正直な感想です。

本記事が参考になれば幸いです。

コメント

タイトルとURLをコピーしました