ProviderとStateProviderについて

はじめに

こんにちは。
今回はRiverpodについて触れてみたいと思います。
Flutterでステートの管理と言えばRiverpodと言えるほど、もはや浸透してきているライブラリですけれども、いざ調べてみても日本語の記事は情報が古かったり、結局特定の仕様に対してどの種類がベストプラクティスなのかよく分からない方が多いと思います。私もその一人です。
ですので自分のアウトプットも含めてRiverpodをざっくり説明していきます。

Providerの種類

さきほど種類と言いましたが、なんの種類かというとProviderというRiverpodの中心的な役割のことです。Providerで値だったりオブジェクトをラップすることで、その状態を監視することができます。
監視というのは値やオブジェクトが変化している様子を常に参照できることだと思ってください。この監視という言葉はRiverpodの記事を漁っているとよく出てきますのでイメージしておくとよいです。
さて、話は逸れましたがProvider(監視の仕方)には以下の種類があります(2023年3月時点)。

これだけの種類があるので違いが分からないのも無理ないでしょう。
Providerによってメリット・デメリットがあり仕様によって組み合わせたり、あえて一つのProviderで統一するなんてこともあります。
今回は中でもProviderStateProviderについて解説していきます。
きちんと違いを把握してクリーンな設計を目指しましょう。
それではソースを見ながらじっくり解説していきます。

ディレクトリ構成

ディレクトリ構成は以下のようになっています。

 lib
│   ├── Widget
│   │   └── ReButton.dart
│   ├── main.dart
│   └── ui
│       ├── HomePage.dart
│       ├── models
│       │   ├── product.dart
│       │   └── suggestion.dart
│       ├── pages
│       │   ├── cartProvider.dart
│       │   ├── change_notifier_page.dart
│       │   ├── data
│       │   │   ├── cart_provider_page.dart
│       │   │   └── cart_state_notfier.dart
│       │   ├── future_provider.dart
│       │   ├── provider_page.dart
│       │   ├── state_notifier_page.dart
│       │   ├── state_provider_page.dart
│       │   └── stream_provider_page.dart
│       └── service
│           ├── api_service.dart
│           └── stream_service.dart

静的かつ使い回しするウィジェットはWidget配下、動的なページはui配下に入れております。ui配下はMVCモデルに則ってmodelとpageとdata分かれております。またservice配下は非同期で取得するデータをまとめております。

メイン画面仕様

今回は以下のようにボタンでそれぞれのproviderのページに遷移する形でおこなっていきます。今回は上2つのProviderとStateProviderしか使わないので、ボタンは2つだけで大丈夫です。

メイン画面

ソースは以下のようになってます(import関連は省略します。flutterの自動補完は優秀なので適宜修正してみてください。)

//main.dart
void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context,) {
    return MaterialApp(
      title: 'Riverpodいろいろ',
      theme: FlexThemeData.light(scheme: FlexScheme.bigStone),
      darkTheme: FlexThemeData.light(scheme: FlexScheme.mandyRed),

      themeMode: ThemeMode.system,
      home: const HomePage(),
    );
  }
}
//homepage.dart
class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(
    BuildContext context,
  ) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Widget Templete'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            Rebutton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => ProviderPage(
                        color: Theme.of(context).colorScheme.primary,
                      ),
                    ),
                  );
                },
                text: "Provider"),
            const SizedBox(
              height: 10,
            ),
            Rebutton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => StateProviderPage(
                      color: Theme.of(context).colorScheme.secondary,
                    ),
                  ),
                );
              },
              text: "State Provider",
              color: Theme.of(context).colorScheme.secondary,
            ),
            /////////////////////////////////////////////////////
            //↓↓↓↓↓↓↓↓↓↓以下ボタンの記述が続きます↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓//
            /////////////////////////////////////////////////////
          ],
        ),
      ),
    );
  }
}

注目してほしいのはMyAppメソッドをproviderScopeでラップすることです。
これをしないとproviderが使えないので必ず行いましょう

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

さて、providerが使えるようになったところで、さっそく一番ベーシックなproviderから説明していきます。

Provider

ソース

//provider_page.dart

//プロバイダーの宣言
final valueProvider = Provider<int>((ref) => 50);

class ProviderPage extends ConsumerWidget {
  const ProviderPage({
    super.key,
    required this.color,
  });

  final Color color;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: color,
        title: const Text("Provider"),
      ),
      body: Consumer(
        builder: (BuildContext context, WidgetRef ref, Widget? child) {
          return Center(
            child: Text(
              'The Value is ${ref.watch(valueProvider)}',
            style: Theme.of(context).textTheme.headline4,
            ),
          );
        },
      ),
    );
  }
}

providerの宣言の仕方

ここでは単純にproviderでどうすれば値を参照できるのかを見ていきます。
まずproviderを宣言します。ここではvalueProviderにProviderのオブジェクトを入れています。providerの状態を見たいときはvalueProviderを参照することになります。今回は数値型を扱うので型推論にintを入れます。
下記の場合int型の値を返す、という意味になります。
なくても大丈夫ですが通常はオブジェクト型を扱うことが多いので可読性を考慮するなら使用を推奨します。
Providerのオブジェクトの引数にrefが引数で、戻り値が50のメソッドが入っております。
ここがややこしいですが、50には値だけでなくオブジェクトが入ったりします。そしてrefから値を参照することができます(参照の仕方は後で説明します)。

final valueProvider = Provider<int>((ref) => 50);

ここで注目してほしいのがproviderを宣言している箇所がグローバルであるということです。これはつまりどこからでもproviderにアクセスできる事を意味します。これまでのproviderでは、providerに包まれたツリー以外の箇所からアクセスするとエラーを吐いてしまっていたので、一つならいいですけど、複数個providerを宣言するとごちゃごちゃしてしまうので、非常に直感的でわかりやすくなりました。
ここではRiverpodのproviderの宣言について解説しました。同じような記述が今後何度も出てくるので、実際にコードを書いて覚えちゃいましょう。

値の参照の仕方

宣言したproviderを参照してみましょう。
参照する前にいくつかすることがあります。
参照するウィジェットのクラスをConsumerウィジェットで継承します。

class ProviderPage extends ConsumerWidget {
////////////////処理記述///////////////////
}

これでProviderの値を参照するウィジェットを指定することができました。
次にbuilderの引数に注目します。通常BuilderContextに加え、さらに
WidgetRefを追加します。

Widget build(BuildContext context, WidgetRef ref) {
////////////////処理記述///////////////////
}

これで準備が整いました。あとは参照するだけです。
WidgetRefで宣言されているrefをwatchで参照します。

      Center(
            child: Text(
              'The Value is ${ref.watch(valueProvider)}',
            style: Theme.of(context).textTheme.headline4,
            ),

ref.watch(valueProvider)で値を参照します。
watchは読み込まれた瞬間に再度ビルドし直すことを意味します。
つまりbuiderメソッドが再度読み込まれるということですね。
以下のような表示が出たら完璧です。

Providerデモ

StateProvider

さて、値の参照の仕方が分かったところで、動的にUIが変化するパターンも見ていきます。

ソース

//state_provider_page.dart

final valueStateProvider = StateProvider<int>((ref) => 50);

class StateProviderPage extends ConsumerWidget {
  const StateProviderPage({
    super.key,
    required this.color,
  });

  final Color color;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final value = ref.watch(valueStateProvider);
    ref.listen<int>(valueStateProvider, (prev, curr) {
      if (curr == 65) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Value is 65')),
        );
      }
    });
    return Scaffold(
      appBar: AppBar(
        backgroundColor: color,
        title: const Text("State Provider"),
      ),
      body: Center(
        child: Column(
          children: [
            Text(
              'The value is $value',
              style: Theme.of(context).textTheme.headline4,
            ),
            const SizedBox(
              height: 20,
            ),
            ElevatedButton(
              onPressed: () {
                ref.read(valueStateProvider.notifier).state++;
              },
              child: const Text('Increment'),
            ),
            ElevatedButton(
              onPressed: () {
                ref.invalidate(valueStateProvider);
              },
              child: const Text('Invalidate'),
            )
          ],
        ),
      ),
    );
  }
}

見た目は以下のようになります。

State Provider

今回は値が動的に動くようなTextウィジェットと、インクリメントボタン、初期化ボタンを用意しました。ではソースを細かく見ていきます。
ソースは先ほどと同様にProviderをint型の初期値50で宣言してます。今度はstateProviderですので、StateProvider型で宣言します。

final valueStateProvider = StateProvider<int>((ref) => 50);

Providerは値を読み取ることしかできませんが、StateProviderは値をウィジェット内で更新することができます
更新するということはイベントに応じて値を変更したりするロジックが動き、再度ビルドしてウィジェットに反映させるんだろうなーと想像がつくと思います。
ビルド直下のロジックは置いといて、まずreturn直下のウィジェットを見ていきます。
まず最初にインクリメントボタンから見ていきましょう。

readとwatchの違い

        onPressed: () {
                ref.read(valueStateProvider.notifier).state++;
              },

インクリメントボタン内にはonPressedでメソッドを発火させてます。
んrefは先ほどproviderで学んだように初期値50の値が格納されており、これをreadで動かしています。
watchに加えてreadが登場しました。readとはwatchとよく比較される値の参照の仕方です。
watchとの大きな違いはwatchは読み込まれた瞬間に再度ビルドされますがreadはされません
たったこれだけです。read内に記述してあるnotifierはパッケージ内のプロパティであり、値の変更を通知させる意味を持っています。state++のstateはメソッドであり、今回はインクリメントするだけなのでstate++で問題ありません。
再度ビルドしなくても値は常に更新されるため、providerに変更を加える処理はwatchとreadどちらでも大丈夫ですが、読み込まれた瞬間にレンダリングされた後の値を表示したい時はwatchを使ってください。
例えばTextウィジェット内のvalueはwatchで定義されてますが、これをreadに書き換えてみてください。

final value = ref.read(valueStateProvider);

valueは変数ですので再度ビルドされない限り値は更新されません。つまりこの状態でインクリメントをクリックしても数字のUIは更新されないことがわかると思います。公式ドキュメントによるとreadは非推奨となっているため、基本watchで大丈夫だと思いますが、使用する場面もあるため、違いを理解しておくのも重要です。

invalidateとautoDispose

invalidateボタンはproviderをリフレッシュすることができます。ユーザーの入力で状態を初期値に戻したい場合はinvalidateを使ってください。

ref.invalidate(valueStateProvider);

しかしユーザーが意図しない形で状態を初期値に戻したい場合が設計上存在すると思います。Providerは状態をキャッシュするので、ユーザーがリフレッシュするか、providerを読み込んだ瞬間に初期値に戻してくれるようなものが必要です。ここでautoDisposeが出てきます。providerの宣言箇所にautoDisposeを追加してみてください。

final valueStateProvider = StateProvider.autoDispose<int>((ref) => 50);

これを追加し、再度インクリメントボタンをクリックし、最初のホームページの画面に戻ってください。そして再度同じページを開きます。そうするとautoDisposeにより値が50に戻ってることがわかります。

最後に

今回はProviderとStateProviderについて解説しました。
加えてreadとwatchの違いやリフレッシュ周りの違いについても解説しました。記事が長くなるため、割愛してますが、Providerは他にも4種類近くあり、紹介していない記述の仕方やが多数ありますので、今後さらにアップデートしていきます。

この記事が気に入ったらサポートをしてみませんか?