Flutterの状態管理パッケージ Provider・Riverpod(flutter_riverpod)を使ってそれぞれのコードを見比べてみた

2024.02.14

こんにちは、ゲームソリューション部のsoraです。
今回は、Flutterの状態管理パッケージ Provider・Riverpod(flutter_riverpod)を使ってそれぞれのコードを見比べてみたことについて書いていきます。

今回実装したアプリ

今回実装したアプリは以下です。
プロジェクト作成時のコードを少し変えたものです。
このアプリを状態管理パッケージごとにそれぞれ実装して、見比べていきます。

なぜ状態管理パッケージを使うのか

まずは、なぜ状態管理パッケージを使うのかという話からします。

状態管理パッケージを使わない場合、状態が変化するものに対しては、StatefulWidgetを継承したWidgetを作成して、その中でStateクラスを使用します。
この場合、1つのStateを複数の他のWidgetで参照する場合に、親ウィジェットを経由して状態を渡す必要があるのですが、これだと状態を使用したいWidgetにたどり着くまでの部分の全てのWidgetに更新が入ってしまい処理効率が悪いです。 またコードも複雑化します。

Widgetツリーの話なのですが、以下記事がわかりやすかったです。
[Flutter] Provider - 今回のブログポストではFlutterでグローバル状態(State)、またはウィジェットたちの間で状態(State)を共有するためProviderを使う方法について説明します。

これが状態管理パッケージを使用すると、各Widgetへ直接状態の受け渡しをすることができ、無駄な箇所の更新を入れずに効率よく再描画することができるようになります。
そのため、Flutterでは状態管理パッケージを使用することが一般的になっているとのことです。

主な状態管理パッケージ

主な状態管理パッケージは以下です。
詳細な解説については、私もまだ使い始めたばかりのため、今回は割愛します。

Provider
公式も推奨している状態管理パッケージ

Riverpod
機能的にProviderの上位互換である状態管理パッケージ
Riverpodの中でも、3パターン存在する
- Flutterで使用する場合、flutter_riverpod
- Dart単体で使用する場合、riverpod
- Flutter Hooksと併用する場合、hooks_riverpod

今回は、flutter_riverpodを使用します。

状態管理パッケージなし

まず、状態管理パッケージなしのコードは以下です。
プロジェクト作成時のコードを少し変えただけなので、説明は割愛します。

main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Provider test'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: _incrementCounter,
                    child: const Text('Count Up'),
                  ),
                ]
            )
          ],
        ),
      ),
    );
  }
}

状態管理パッケージ Providerを使用

次にProviderを使用して実装します。
以下ページをもとにパッケージをインストールします。
provider | Flutter package

flutter pub add provider

Providerを使用して作成したコードは以下です。
細かい部分の説明はコード内のコメントに記載していますが、StatefulWidget+Stateの形ではなく、StatelessWidgetで記述できていることがわかります。

main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'model.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(),
      home: const MyHomePage(),
    );
  }
}
// ここまで状態管理パッケージなしのパターンと同じ

// Providerを使うことで、StatefulWidget+Stateの形ではなく、StatelessWidgetになっている
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    // 状態の変更を検知するChangeNotifierProvider
    return ChangeNotifierProvider<CounterModel>(
      // model.dartで定義したモデルを作成
      create: (_) => CounterModel(),
      child: Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: const Text('Provider test'),
          ),
          // 状態の変更を受け取って再描画する範囲をConsumerで囲む
          body: Consumer<CounterModel>(builder: (context, model, child){
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Text(
                    // model.dartで定義したクラスのインスタンスから値を取得
                    model.counter.toString(),
                    style: Theme.of(context).textTheme.headlineMedium,
                  ),
                  Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        ElevatedButton(
                            child: const Text('Count Up'),
                            onPressed: (){
                              // model.dartで定義したクラスのインスタンスのメソッドを実行
                              model.incrementCounter();
                            },
                        ),
                      ],
                  ),
                ],
              ),
            );
          })
      ),
    );
  }
}

main.dartとは別で、モデルを定義するmodel.dartを作成しています。

model.dart

import 'package:flutter/material.dart';

// クラスの変更を監視できるProviderであるChangeNotifierを継承したクラスを定義
class CounterModel extends ChangeNotifier {
  int _counter = 0;
  int get counter => _counter;

  void incrementCounter() {
    _counter++;
    // notifyListeners()で値の変更をリスナーへ通知
    notifyListeners();
  }
}

状態管理パッケージ Riverpod(flutter_riverpod)を使用

最後にRiverpod(flutter_riverpod)を使用して実装します。
以下ページをもとにパッケージをインストールします。
flutter_riverpod | Flutter package

flutter pub add flutter_riverpod

Riverpod(flutter_riverpod)を使用して作成したコードは以下です。
細かい部分の説明はコード内のコメントに記載していますが、こちらもProviderパッケージを使用したパターンと同様、StatefulWidget+Stateの形になっていないことがわかります。

flutter_riverpodには、ProviderやNotifierProviderなどの様々な種類のProviderが存在します。
詳細について知りたい方は、以下の記事が各種類に対してコード付きで説明されていて、わかりやすかったのでおすすめです。
Riverpod 2.x の Provider まとめ

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Notifierクラスを定義
class Counter extends Notifier<int> {
  final count = 0;
  @override
  int build() {
    return count;
  }
  void increase() {
    state++;
  }
}
// NotifierProviderをグローバルに定義
final countProvider = NotifierProvider<Counter, int>(() {
  return Counter();
});


void main() {
  runApp(
    // Providerを全体で利用可能にするためにProviderScopeでスコープを指定
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Provider test'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              // ref.watch(StateProvider)で対象のProviderの状態取得と変更検知
              // ref.read(StateProvider)で対象のProviderの状態取得のみ(変更検知は行わない)
              "${ref.watch(countProvider)}",
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    child: const Text('Count Up'),
                    onPressed: (){
                      // Notifier内でメソッドを使用してカウントを増加させる
                      ref.watch(countProvider.notifier).increase();
                    },
                  ),
                ]
            ),
          ],
        ),
      ),
    );
  }
}

最後に

今回は、Flutterの状態管理パッケージ Provider・Riverpod(flutter_riverpod)を使ってそれぞれのコードを見比べてみたことを記事にしました。

個人的にRiverpodが一番書きやすくて読みやすいと思ったので、今後は一旦Riverpod(flutter_riverpod)を使っていこうかなと思います。
後々はFlutter Hooksと合わせて、Riverpod(hooks_riverpod)も使ってみようと思います。

本記事がどなたかの参考になると幸いです。