【Flutter】状態管理②:providerからRiverpodへ。カウンターアプリを例に
はじめに
前回こちらの記事で、状態管理に関して、statefulWidget + setStateからProviderまでを記載した。今回は、ProviderからRiverpodまでを見ていく。
Providerの課題
-
コンテキスト依存: **
Provider
はBuildContext
**に強く依存しており、これがコードのテストや再利用を難しくしている。特にウィジェットツリーの外や非UIのコードから状態にアクセスしようとする場合、この依存性が問題となる。 -
不変性: **
Provider
を使う場合、変更可能な状態を持つことができるが、これがバグの原因になり得る。理想的には、状態は不変であるべきだが、Provider
**だけではこれを強制することができない。 -
初期化の複雑さ: 特定の状態が依存している他の状態がある場合、**
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),
),
);
}
}
コンテキスト依存
MyHomePage
がBuildContext
を使って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
で管理するデータの型>が必要。 -
counterProvider
はStateNotifierProvider<CounterNotifier, int>
型のオブジェクトで、その実体はCounterNotifier
クラスのインスタンス。CounterNotifier
はStateNotifier<int>
を継承しており、整数型の状態を管理する。
コールバック関数
-
StateNotifierProvider
のコンストラクタに渡されるコールバック関数では、CounterNotifier
のインスタンスが作成され返される。この関数は、プロバイダーが初めて呼び出される際に実行され、状態の初期化を行う。
ProviderRef
- コールバック関数の引数
ref
は、他のプロバイダーからの状態やサービスを参照する際に使用されるProviderRef
オブジェクト。
StateNotifier
-
CounterNotifier
はStateNotifier
を継承したクラスで、カウンターの状態(この場合は整数値)を管理する。コンストラクタで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がデータの変更を監視する必要がない時。アクション(例えばボタンタップ時の処理)をトリガーするときなど。
続きは、こちらで記載しています
Discussion