- 状態管理
- 日本における開発事例が多い
- Flutterの状態管理の思想からは外れている
- 依存性の注入によるサービスロケータパターン
- 基本機能を提供するパッケージ
- Providerのコードを生成するパッケージ
- 静的解析を行うパッケージ
パッケージ名 |
内容 |
riverpod |
Flutterに依存しない |
flutter_riverpod |
Flutter関連ならこれを使うべし |
hooks_riverpod |
flutter_riverpod + Reactのフック |
機能 |
内容 |
Provider |
状態のキャッシュ |
RefとWidgetRef |
状態の提供 |
ConsumerWidget |
状態を使う |
- ConsumerWidgetのbuild()メソッドの引数は, WidgetRefが渡されて, それを介して状態を取得する.
- Providerの状態が変化するとbuild()メソッドが再度呼び出される.
final greetProvider = Provider((ref: Ref) {
return 'Hello Flutter';
});
class MyTestPage extends ConsumerWidget {
const MyTestPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final greet = ref.watch(greetProvider);
return Center(child: Text('${greet}');
}
}
NotifierとNotifierProviderを用いたRiverpod
- build()メソッドで初期値を与える
- 任意の更新関数を定義する
- 状態変数はstate
// Provider
class CounterNotifier extends Notifier<T> {
@override
T build() => 0; // 初期値
// 更新関数
void increment() {
state = state + 1;
}
}
// NotifierProvider
final counterNotifierProvider = NotifierProvider<CounterNotifier, int>() { return CounterNotifier(); });
// Consumer
class MyTestPage extends ConsumerWidget {
const MyTestPage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterNotifierProvider);
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("You have pushed the button this many times"),
Text('$counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterNotifierProvider.notifier).increment();
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Providerコードを生成するパッケージ(推奨)
- 目的: Riverpodによるボイラーテンプレートを減らす
- riverpod_generator
- riverpod_annotation
- build_runner
- riverpod_lint: custom_lintを内部で利用する
- '$ flutter pub add --dev riverpod_generator riverpod_lint build_runner'
- '# flutter pub add flutter_riverpod riverpod_annotation'
// コードを生成しない通常パターン
final greetProvider = Provider((ref: Ref) {
return 'No generator for provider code';
});
// コード生成を使用するパターン
@riverpod
String greet(GreetRef ref) {
return "Use generator for provider code';
});
- Providerに関するコードを記述する際の意思決定が減る
- Providerへ渡すパラメータの制限がなくなる
- Providerの変更がホットリロードできる
// lib/test_func_provider.dart
part 'test_func_provider.g.dart';
@riverpod
String greet(GreetRef ref) {
return 'Function base provider';
}
- 生成後のProvider名は
greetProvider
になる
- 生成方法:
$ flutter packages pub run build_runner build
// lib/test_class_provider.dart
part 'test_class_provider.g.dart';
@riverpod
class CounterNotifier extends _$CounterNotifier {
//class CounterNotifier extends Notifiler<T>
@override
int build() => 0;
void increment() {
state = state + 1;
}
}
- Future型やStream型を提供するProvider
@riverpod
Future<String> asyncGreet(AsyncGreetRef ref) async {
await Future.delayed(const Duration(seconds: 1)); // python asincioのasyncio.sleep()と同じ
return 'Async provider of riverpod with function base';
} // ConsumerWidgetで取得する際は, AsyncValue型でラップされる
@riverpod
class CounterNotifier extends _$CounterNotifier {
@override
Future<int> build() async {
await Future.delayed(const Duration(seconds: 1));
return 0;
} // ConsumerWidgetで取得する際は, AsyncValue型でラップされる
}
非同期Providerの戻り値はAsyncValue型
- 非同期の値を安全に扱うためのヘルパークラス
- AsyncValue
- Tは状態を決めるキーワード: e.g. T=String: 'loading', 'error', 'data'
// asyncGreetProviderを監視するWidget
class HomePage extends ConsumerWidget {
const HomePage({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// Providerの戻り値がFuture<String>ではなく, AsyncValue<T>クラスでラップされている.
final AsyncValue<String> greet = ref.watch(asyncGreetProvider);
return Center(
child: greet.when(
loading: () => const Text('Loading'),
error: (e, st) => Text(e.toString()),
data: (greet) => Text(greet),
),
);
}
}
クラスベースの非同期型Providerの更新にもAsyncValueを用いる
@riverpod
class CounterNotifier extends _$CounterNotifier {
@override
Future<int> build() async {
await Future<void>.delayed(const Duration(seconds: 1));
return 0;
}
void increment() async {
final currentValue = state.valueOrNull;
// AsyncValueがdata以外はnullが返る
if (currentValue == null) {
return;
}
state = const AsyncLoading(); // 状態をloadingにする
await Future<void>.delayed(const Duration(seconds:1));
state = AsyncValue.data(currentValue + 1);
}
}
- AsyncValue型は, 次の3つの状態のどれかをとる. loading, error, data
Raw型によるラッピングでAsyncValue型の使いづらさを解消
- AsyncValue型は, 複数の非同期Providerが絡むと使いづらい
// e.g. 使いづらい例
// async provider: 1
@riverpod
Future<int> fakeFirstApi(FakeFirstApiRef ref) async {
await Future.delayed(const Duration(seconds: 1));
return 1;
} // fakeFirstApiProvider
// async provider: 2
@riverpod
Future<int> fakeSecondApi(FakeSecondApiRef ref) async {
await Future.delayed(const Duration(seconds: 1));
return 2;
} // fakeSecondApiProvider
// integrated async provider with 1 and 2
@riverpod
Future<int> fakeSumApi(FakeSumApiRef ref) async {
// AsyncValue<int>が扱いづらい
final AsyncValue<int> firstApiResult = ref.watch(fakeFirstApiProvider);
final AsyncValue<int> secondApiResult = ref.watch(fakeSecondApiProvider);
await Future.delayed(const Duration(seconds: 1));
return firstApiResult.data + secondApiResult.data;
} // fakeSumApiProvider
- 解決策: Providerの戻り値の型がAsyncValueではなく, Futureだと良い.
- Providerの戻り値であるFutureをRaw型でラップすると, 呼び出し側でFutureが得られる.
// e.g. 改良版
// async provider: 1
@riverpod
Raw<Future<int>> fakeFirstApi(FakeFirstApiRef ref) async {
await Future.delayed(const Duration(seconds: 1));
return 1;
} // fakeFirstApiProvider
// async provider: 2
@riverpod
Raw<Future<int>> fakeSecondApi(FakeSecondApiRef ref) async {
await Future.delayed(const Duration(seconds: 1));
return 2;
} // fakeSecondApiProvider
// integrated async provider with 1 and 2
@riverpod
Raw<Future<int>> fakeSumApi(FakeSumApiRef ref) async {
final int firstApiResult = await ref.watch(fakeFirstApiProvider);
final int secondApiResult = await ref.watch(fakeSecondApiProvider);
await Future.delayed(const Duration(seconds: 1));
return firstApiResult + secondApiResult;
} // fakeSumApiProvider
- ref.watch()メソッド
- ref.read()メソッド
- build()メソッド内部でwatchするとProviderの状態が変化した場合に, build()メソッドが走る.
- 複数のwatch()メソッドによる状態変化のループによって, 無限ループに陥らないように注意すること.
- その時点でのProviderの状態を取得する
- build()メソッドは走らない.
- onPress()内部などユーザートリガーのイベントやStateのライフサイクルイベントで使う.
- 状態(集合)のうち, 指定した一部の要素だけ変化した場合にbuild()メソッドを走らせることで再描画を極力減らす.
// 状態 Point
class Point {
Point(this.x, this.y);
int x;
int y;
}
// Provider
@riverpod
class AsyncPointNotifier extends _$AsyncPointNotifier {
@override
Future<Point> build() {
await Future<void>.delayed(const Duration(seconds: 1));
return Point(0,0);
}
@override
void plusOne() async {
final currentValue = state.valueOrNull;
if (currentValue == null) {
return;
}
state = const AsyncLoading();
await Future<void>.delayed(const Duration(seconds: 1));
state = AsyncValue.data(Point(currentValue.x + 1, currentValue.y + 1));
}
}
// Providerを使う側
class HomePage extends Consumerwidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final x = await ref.watch(asyncPointNotifier.select((point) => point.x));
...
}
}
- コード生成したProviderは, 購読されなくなるとGCで消される.
- 複数の画面を跨いで状態を共有したいケースでは, 常にメモリ上にProviderを配置しておく.
- アプリ起動中, 常にメモリ上にProviderを配置したいケースでは@Riverpotを用いる.
@Riverpod(keepAlive: true)
class AsyncCounterNotifier extends _$AsyncCounterNotifier {
@override
Future<int> build() => 0;
void increment() async {
final currentValue = state.valueOrNull;
if (currentValue == null) {
return;
}
state = const AsyncLoading(); // loading state
await Future<void>.delayed(const Duration(seconds: 1));
state = AsyncValue.data(currentValue + 1);
}
}
任意のタイミングでProviderを再構築したい場合
class HomePage extends ConsumerWidget {
HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
...
ref.refresh(counterNotifierProvider);
...
}
}
@riverpod
String greet(GreetRef ref, String message) {
return 'Hello $message';
}
@riverpod
Raw<Future<String>> asyncGreet(AsyncGreet ref, String message) async {
await Future<void>.delayed(const Duration(seconds: 1));
return 'Hello $message';
}
@riverpod
class AsyncGreetNotifier extends _$AsyncGreetNotifier {
@override
Future<String> build(String message) async {
await Future<void>.delayed(const Duration(seconds: 1));
return 'Good $message';
}
void asyncAddName(String name) async {
final currentValue = state.valueOrNull;
if (currentValue == null) {
return;
}
state = const AsyncLoading(); // state -> loading
await Future<void>.delayed(const Duration(seconds: 1));
state = AsyncValue.data("${currentValue}, ${name}!");
}
}
class HomePage extends ConsumerWidget {
const HomePage({super.key, required this.name});
final String name;
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncGreetNotifier = asyncGreetNotifierProvider('morning'); // Good morning
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Async greeting demo'),
),
body: Center(
child: Column(
mainAxisAligment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have greeted below your friends.'),
Text(
"${ref.watch(asyncGreetNotifier).switch(
loading: () => const Text('Loading'),
error: (e, st) => Text(e.toString()),
data: (greetToFriends) => Text('$greetToFriends'),
)",
style: theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await ref.read(counterNotifier.notifier).asyncAddName("Ben"); // Future<void>
},
tooltip: "Add friend",
child: const Icon(Icons.add),
),
);
}
}