【Flutter】状態管理②:providerからRiverpodへ。カウンターアプリを例に

2024/04/11に公開

はじめに

前回こちらの記事で、状態管理に関して、statefulWidget + setStateからProviderまでを記載した。今回は、ProviderからRiverpodまでを見ていく。

https://kazulog.fun/dev/flutter-statefulwidget-setstate-provider/

Providerの課題

  1. コンテキスト依存: **ProviderBuildContext**に強く依存しており、これがコードのテストや再利用を難しくしている。特にウィジェットツリーの外や非UIのコードから状態にアクセスしようとする場合、この依存性が問題となる。
  2. 不変性: **Providerを使う場合、変更可能な状態を持つことができるが、これがバグの原因になり得る。理想的には、状態は不変であるべきだが、Provider**だけではこれを強制することができない。
  3. 初期化の複雑さ: 特定の状態が依存している他の状態がある場合、**Provider**でこれを扱うのは比較的複雑。

カウンターアプリの例で見ていく

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

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => CounterProvider(),
      child: MaterialApp(
        home: MyHomePage(),
      ),
    );
  }
}

class CounterProvider with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class MyHomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    final counter = Provider.of<CounterProvider>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Provider Counter Example'),
      ),
      body: Center(
        child: Text('Counter: ${counter.count}'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counter.increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

コンテキスト依存

MyHomePageBuildContextを使ってCounterProviderにアクセスしている。このコンテキスト依存は、特に状態にアクセスしたいがウィジェットツリー内の位置が不明確な場合に問題を引き起こす。

不変性の問題=オブジェクトの上書き

CounterProvider内で_countが変更可能な状態。Flutterは反応的なフレームワークなので、通常はnotifyListeners()を呼び出してウィジェットツリーに変更を通知する。しかし、もし何らかの理由でnotifyListeners()が呼ばれなかった場合、UIは実際の状態と同期しなくなる。

初期化の複雑さ

このシンプルなカウンターアプリでは明確には示されないが、複数のカウンターが相互に依存しているより複雑なシナリオでは、Providerでこれらの依存関係を管理するのは煩雑になる。例えば、あるカウンターが別のカウンターの値に依存して初期化されるべき場合、この依存関係を正確にコード化する必要があるが、Providerだけではその管理が直感的ではない可能性がある。

Riverpodの書き方

Riverpodはこれらの問題を解決しようと設計されたライブラリ。Providerの作者によって開発され、Providerのアイデアを基にしながら改善された。

Riverpodを使用して同様のカウンターアプリを実装すると、以下のようになる。

ルートにProviderScopeを追加する

ProviderScope で上位ツリーのWidgetを囲むと、下位ツリーのWidgetでProviderを呼び出すことができるようになる。以下のコードではMyApp() をProviderScope で囲むことでMyApp以降のWidgetでProviderを呼び出すことができるようにしている。

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

Providerをグローバル変数として定義する

グローバル変数としてのProvider

  • counterProviderはアプリのどこからでもアクセス可能なグローバル変数。これにより、状態を管理するCounterNotifierへの参照がどこからでも可能になる
  • StateNotifierProvider を定義する時はStateNotifierProvider の後に<管理するStateNotifier のサブクラス名, StateNotifier で管理するデータの型>が必要。
  • counterProviderStateNotifierProvider<CounterNotifier, int>型のオブジェクトで、その実体はCounterNotifierクラスのインスタンス。CounterNotifierStateNotifier<int>を継承しており、整数型の状態を管理する。

コールバック関数

  • StateNotifierProviderのコンストラクタに渡されるコールバック関数では、CounterNotifierのインスタンスが作成され返される。この関数は、プロバイダーが初めて呼び出される際に実行され、状態の初期化を行う。

ProviderRef

  • コールバック関数の引数refは、他のプロバイダーからの状態やサービスを参照する際に使用されるProviderRefオブジェクト。

StateNotifier

  • CounterNotifierStateNotifierを継承したクラスで、カウンターの状態(この場合は整数値)を管理する。コンストラクタでsuper(0)としているのは、カウンターの初期値を0に設定していることを意味する。

状態の更新

  • incrementメソッドは、カウンターの値をインクリメントする。stateは現在のカウンター値を表し、state++により状態が更新される。これはイミュータブルな状態管理であり、状態を更新するために新しい状態オブジェクトが作成され、古い状態オブジェクトは破棄される。StateNotifierは状態が更新されると、それを購読しているウィジェットに通知し、UIの再描画をトリガーする。
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() {
    state++;
  }
}

refの使用例

例えば、CounterNotifierが起動時に設定プロバイダーから初期値を取得する場合、以下のようにrefを使用してその値にアクセスすることができる。

この例では、CounterNotifierのコンストラクタで初期値を受け取り、その初期値はsettingsProviderから取得される。ref.readメソッドを使ってsettingsProviderの現在の値にアクセスし、その値をCounterNotifierの初期化に使用している。refはプロバイダーのコールバック内で他のプロバイダーにアクセスするためのキーとなるオブジェクト

final settingsProvider = Provider<Settings>((ref) => Settings());

final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  // settingsProviderから設定を取得
  final settings = ref.read(settingsProvider);
  // 設定から初期カウンター値を取得して、CounterNotifierを初期化
  return CounterNotifier(settings.initialCounterValue);
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier(int initialCounter) : super(initialCounter);

  void increment() {
    state++;
  }
}

Providerからデータを取得する

ConsumerWidget

  • ConsumerWidgetはRiverpodで提供される特別なウィジェットで、これを継承することで、ウィジェットがProviderからデータを読み取り、そのデータの変更に基づいて再ビルドするように設定できる。

WidgetRef

  • buildメソッドに渡されるWidgetRef引数は、Providerからデータを読み取るために使用される。WidgetRefは、BuildContextの代わりに使用され、Providerのデータへのアクセスや、ウィジェットのライフサイクルに関する追加の機能を提供する。

watch() と read()

  • ref.watch(), ref.read()メソッドに指定する引数は、グローバルに定義されたプロバイダーのオブジェクト。今回の場合、ref.watch(counterProvider)を呼び出すことで、counterProviderが管理する状態(この場合はカウンターの値)にアクセスし、その値の変更を監視することができる。
  • ref.watch():UIがデータの変更を監視する必要がある時
  • ref.read():UIがデータの変更を監視する必要がない時。アクション(例えばボタンタップ時の処理)をトリガーするときなど。

続きは、こちらで記載しています
https://kazulog.fun/dev/flutter-state-management-provider-to-riverpod/

Discussion