はじめに
前回こちらの記事で、状態管理に関して、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 {
@override
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 {
@override
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がデータの変更を監視する必要がない時。アクション(例えばボタンタップ時の処理)をトリガーするときなど。
increment()の呼び出し
FloatingActionButton
のonPressed
でref.read(counterProvider.notifier).increment()
が呼び出されることで、カウンターの値が増加する。StateNotifier
のサブクラスはWidgetRef
クラスのwatch()
またはread()
の引数にStateNotifierProvider
インスタンスの.notifier
を指定することで取得できる。StateNotifier
は2つの異なるオブジェクトを提供していて、状態(state
)に関しては自身が直接管理しアクセスするため.notifier
は不要だが、その状態を管理するサブクラスのメソッド(状態を変更するロジックなど)にアクセスするには、.notifier
が必要。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: Text('Riverpod Counter Example'),
),
body: Center(
child: Text('Counter: $count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Provider→Riverpodで何が変わり、何が変わらなかったか?
ConsumerWidgetの役割:同じ
- どちらの場合も、
ConsumerWidget
はデータの変更に基づいてUIを更新する役割を持つ。ConsumerWidget
を使用することで、ウィジェットが依存しているデータが変更された際にウィジェットが再構築されるようになる。
WidgetRefの導入
- Providerでは、状態にアクセスする際に
BuildContext
が必要。これは、Flutterウィジェットツリー内でのウィジェットの位置を基にして、状態を見つけ出し参照する。つまり、Provider.of<T>(context)
やcontext.watch<T>()
のように、BuildContext
を使用して状態を取得する。 - Riverpodでは
WidgetRef
の導入により、開発者はウィジェットの位置やBuildContext
を意識することなく、ref.read(provider)
を使って、状態を読み取ったり、ref.watch(provider)
を使って状態の変更を監視したりできる。この際、状態の変更が必要なウィジェットがどこにあるか(従来のBuildContextが果たしていた役割)に関しては、Riverpodが内部的に処理している。
Riverpodの利点
コンテキスト非依存(WidgefRefの導入による)
- Riverpodでは、
BuildContext
に依存せずに状態にアクセスできる。ProviderScope
を使用してアプリ全体に状態を提供し、WidgetRef
を使って状態にアクセスする。これにより、どこからでも容易に状態を参照でき、テストのしやすさも向上する。
不変性の強化=オブジェクトを上書きではなく、捨てて新規作成
StateNotifier
を使うことで、状態の不変性を保証できる。state
はプライベートに保持され、変更する際はstate
プロパティを通じてのみ行う。これにより、UIと状態の一貫性が保たれ、バグのリスクが減少する。
依存関係の明確な管理
- Riverpodでは、プロバイダー間の依存関係が明確に定義できる。プロバイダーを参照するには、
ref.watch
またはref.read
を使用し、これにより状態の依存関係が明確かつ簡潔に管理できる。
CosumerWidgetとConsumerStatefulWidgetの違いと使い分け
ConsumerWidget
ConsumerWidget
はイミュータブル(不変)であり、内部状態を持たない。状態の変更があると、ウィジェット全体が再ビルドされる。ConsumerWidget
を継承したクラスでは、build
メソッドがWidgetRef
を引数として受け取る。- 状態管理が不要なケース(静的なコンテンツの表示・外部データの単純な表示など)は
ConsumerWidget
を使う方がシンプルでメンテナンスが容易。 StatelessWidget
なWidget
でプロバイダーを使いたい場合に使用。Consumer
でラップせずに使うとWidget
全体が再ビルドの対象になる点に注意。
class MyConsumerWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
ConsumerStatefulWidget
ConsumerStatefulWidget
とConsumerState
はStatefulWidget
とそのState
のRiverpod対応版。主な違いは、ConsumerState
にref
オブジェクトがプロパティとして組み込まれている点。これにより、ConsumerState
内でいつでもref
を使ってRiverpodの機能にアクセスできるようになる。ConsumerWidget
ではbuild
メソッドの引数としてref
が渡されるが、ConsumerStatefulWidget
ではそのような引数は渡されない。ref
はConsumerState
のプロパティなので、ConsumerState
クラス内でref
を直接使える。ref.watch
が複数あり、状態更新が頻繁に行われる場合、ConsumerWidget
はその都度再ビルドされるので、パフォーマンスの面で効率が悪くなる可能性がる(Consumer
でラップせずに使えば、状態変更の部分が再ビルドの対象になるので、防ぐことはできる)。このようなシナリオでは、ConsumerStatefulWidget
を使用する方が適切。StatefulWidget
なWidget
でプロバイダーを使いたい場合に使用。
class MyConsumerStatefulWidget extends ConsumerStatefulWidget {
@override
_MyConsumerStatefulWidgetState createState() => _MyConsumerStatefulWidgetState();
}
class _MyConsumerStatefulWidgetState extends ConsumerState<MyConsumerStatefulWidget> {
@override
Widget build(BuildContext context) {
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
Providerの種類6つ
Provider名 | 管理するデータの型 | 目的 |
Provider | 任意 | 外部から変更できないデータを提供し、アプリ全体でアクセス可能にする |
StateNotifierProvider | StateNotifier のサブクラス | 状態の変更とビジネスロジックをカプセル化した状態管理を提供する |
FutureProvider | 任意の Future | 非同期操作の結果を扱い、未来のある時点で利用可能になるデータを提供する |
StreamProvider | 任意の Stream | ストリームからのデータを購読し、時間と共に変化するデータを提供する |
StateProvider | 任意 | シンプルな状態の読み書きを提供し、ウィジェット内で簡単に状態を管理する |
ChangeNotifierProvider | ChangeNotifier のサブクラス | ChangeNotifierを使用して状態の変更をリッスンし、複数のリスナーに状態の変更を通知する |
Provider
- 目的: 外部から変更不可能なデータを提供。プロバイダーの利用者に値の操作はさせず、参照のみをさせたい場合に使用
- 使用例: アプリケーション全体で共有される設定情報を提供。
final configProvider = Provider((ref) => AppConfig(apiBaseUrl: '<https://api.example.com>'));
final config = ref.watch(configProvider);
StateProvider
- 目的: シンプルな状態の読み書きを提供し、ウィジェット内で簡単に状態を管理。プロバイダーの利用者に値の操作と参照をさせたい場合に使用。
- 使用例: UIのトグル状態を管理。
final toggleProvider = StateProvider<bool>((ref) => false);
ref.read(toggleProvider.notifier).state = !ref.read(toggleProvider);
StateNotifierProvider
- 目的: 状態の変更とビジネスロジックをカプセル化した状態管理を提供。
- 使用例: カウンターアプリの状態管理。
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) => CounterNotifier());
ref.read(counterProvider.notifier).increment();
FutureProvider
- 目的: 非同期操作の結果を扱い、将来のある時点で利用可能になるデータを提供。
- 使用例: ネットワークリクエストからデータをフェッチ。
final fooProvicer = FutureProvider<String>((ref) async {
await Future.delayed(const Duration(seconds: 3));
return "foo";
});
class FooWidget extends StatelessWidget {
@override
Widget build(BuildContext context) => Consumer(
builder: (context, ref, child) => ref.watch(fooProvicer).when(
data: (foo) => Text(foo),
error: (err, st) => const Text("Error"),
loading: () => const Text("Loading"),
),
);
}
StreamProvider
- 目的: ストリームからのデータを購読し、時間と共に変化するデータを提供。FutureProviderのStream版
- 使用例: リアルタイムの株価情報を提供。
final fooProvicer = FutureProvider<String>((ref) async {
await Future.delayed(const Duration(seconds: 3));
return "foo";
});
class FooWidget extends StatelessWidget {
@override
Widget build(BuildContext context) => Consumer(
builder: (context, ref, child) => ref.watch(fooProvicer).when(
data: (foo) => Text(foo),
error: (err, st) => const Text("Error"),
loading: () => const Text("Loading"),
),
);
}
ChangeNotifierProvider
- 目的:
ChangeNotifier
を使用して状態の変更をリッスンし、複数のリスナーに状態の変更を通知。
final formStateProvider = ChangeNotifierProvider<FormStateNotifier>((ref) => FormStateNotifier());
コメント