■用途
状態管理を実現するためのアーキテクチャ
状態管理は煩雑なため、大規模開発では以下のアーキテクチャを利用する
- Bloc: Streamを活用したリアクティブな状態管理
- Redux: ActionCreator(ビジネスロジック) -> Action(状態への操作) -> Reducer(状態更新=Store更新)と役割が一方通行で処理を追いやすい
- Provider: 直感的な状態管理
- Riverpod: Providerの強化版
■メリット
- Provider = データの管理、提供者 / Consumer:データの使用者
と役割が明確に分かれているので状態管理がしやすい - Cosumerはデータを監視することでProviderから自動でデータを受け取ることができる。
- アプリの状態を簡単にモック化することができ、テストがしやすい
■導入手順
pubspec.yamlにriverpod関連パッケージを追記し、Pub getを実行
※↑(コードの自動生成が不要なら以下3行は不要)
build_runner: ^2.4.6
riverpod_generator: ^2.3.3
riverpod_analyzer_utils: ^0.3.4
■各モジュールの役割
■Provider
<Provider>
- 値を更新する必要のない定数などの受け渡しに使う
// Provider
final titleProvider = Provider<String>((ref) => 'sampleTitle');
// これと同じ
// final titleProvider = Provider<String>((ref) {
// return 'sampleTitle';
// });
// 参照側(Consumer)
AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(ref.watch(titleProvider)),
),
<StateProvider>
- 値を変更する変数などの受け渡しに使う
// Provider
final countProvider = StateProvider<int>((ref) => 0);
// 更新(Consumer)
floatingActionButton: FloatingActionButton(
onPressed: () => ref.watch(countProvider.notifier).state++,
tooltip: 'Increment',
child: const Icon(Icons.add),
)
// 参照(Consumer)
Text(
ref.watch(countProvider).toString(),
style: Theme.of(context).textTheme.headlineMedium,
)
<FutureProvider>
- 非同期通信のロジックを実行するためのProvider
<FutureProviderFamily>
- FutureProviderに引数を渡すためのProvider
(引数を変えるたびにインスタンスが生成されるため非推奨)
<AutoDisposeFutureProviderFamily>
- FutureProviderFamilyのリソースを自動で開放してくれるProvider
<riverpod_generatorパッケージについて>
特徴:riverpod_generatorを使うと、独自Providerを自動生成してくれる
利用例:AutoDisposeFutureProviderFamilyを使う場合、引数は1つしか渡せないが、複数の引数を利用するよう独自定義できる
<StreamProvider>
- FirebaseStoreなどの更新を監視するために利用できる。
- 他のプロバイダで ref.watch を通じてストリームを監視することができる。
- AsyncValue により loading/error のステートを適切に処理することができる。
- ストリームから出力された直近の値をキャッシュしてくれる(途中で監視を開始しても最新の値を取得することができる)。
- StreamProvider をオーバーライドすることでテスト時のストリームを簡単にモックすることができる。
<StateNotifierProvider>
StateProvider/FutureProvider/StreamProvider全てのユースケースに対応できる
https://zenn.dev/3ta/articles/fb2329ba2ab1dd
■Consumer
<ConsumerWidget>
- RiverpodでのStatelessWidgetに相当
- buildの引数は
Widget build(BuildContext context, WidgetRef ref)
- デメリット:変更があった際にConsumerWidget.buildが実行されてウィジェットツリーが再構築される
<ConsumerStatefulWidget>
- RiverpodでのStatelessWidgetに相当
- buildの引数は
// refはConsumerStatefulWidget内にrefが定義されているので、受け取らずにどこでも使える
Widget build(BuildContext context)
<Comsumer>
- Providerと値を送受信したいウィジェットをComsumerで囲んで使う
- 変更点があったらそのウィジェットだけがrebuild(再構築)される
- ConsumerWidgetやConsumerStatefulWidgetの中で利用しても有効。
class MyHomePage extends StatelessWidget {
...
title: Consumer(
// builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(titleProvider),と同等
builder: (context, ref, child) => Text(ref.watch(titleProvider),
)
),
...
floatingActionButton:
Consumer(
// builder: (BuildContext context, WidgetRef ref, Widget? child) => FloatingActionButton(と同等
builder: (context, ref, child) => FloatingActionButton(
onPressed: () => ref.watch(countProvider.notifier).state++,
tooltip: 'Increment',
child: const Icon(Icons.add),
)
),
- デメリット:Providerを使う箇所全てにConsumerで囲うのは手間がかかる。視認性、保守性が下がる
// title: Text(ref.watch(titleProvider), )
// ↑こう書いていたのを↓このようにConsumerで囲まなくてはいけない
title: Consumer(
// builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(titleProvider),と同等
builder: (context, ref, child) => Text(ref.watch(titleProvider),
)
),
■ref.watch/ref.readとselect
-
ref.watch: 監視し、変更があったらそれを使ってるウィジェットをrebuildする。
-> Textの中でデータバインディングしているなど、監視が必要なときに利用 -
ref.read: 単に読み込むだけで、そのウィジェットのrebuildも走らずに反映される。
-> onTapなど、イベント発生時に値を取得したいだけなら監視は不要。値を取得できればいいのでreadを使う
readとwatchでrebuildが走るタイミングを確認
// watchを使うと該当部分の再描画のたびに以下ログが出力される
Consumer(
// builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(countProvider).toString(),と同等
builder: (context, ref, child) {
print("⭐️rebuild 1");
return Text(
ref.watch(countProvider).toString(),
style: Theme.of(context).textTheme.headlineMedium,
);
}
),
// readを使うと該当部分の再描画の際に以下ログが出力されない
Consumer(
// builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(countProvider).toString(),と同等
builder: (context, ref, child) {
print("⭐️rebuild 2");
return Text(
ref.read(countProvider).toString(),
style: Theme.of(context).textTheme.headlineMedium,
);
}
),
- selectは、watchの中で監視する対象を絞り込める。余計なものを監視しないので、必要な項目が変更されたときだけrebuild走るようになる
selectの実装例
// selectなし: countUpだけじゃなく、countDownに変更があった場合もTextはrebuildされる
Text(ref.watch(countDataProvider).countUp.toString(),)
// selectあり: countDownに変更があった場合はTextはrebuildされない
Text(ref.watch(countDataProvider.state.select((value) => value.state.countUp)).toString(),)
※補足
// 旧参照処理
ref.watch(countProvider).state.toString();
// 新参照処理
ref.watch(countProvider).toString(),
// 旧更新処理
ref.watch(countProvider).state++;
// 新更新処理
ref.watch(countProvider.notifier).state++
■補足: freezedというパッケージの利用
- データクラスの自動生成
- copyWithなどの関数を自動で用意してくれる
- json形式の取り込みを簡単にできる
■サンプルコード
import 'package:riverpod_annotation/riverpod_annotation.dart';
// 自動生成ありの場合
// ターミナルからflutter pub run build_runner buildコマンドで自動生成されるファイル名
// 拡張子前に.gをつける命名規則
// part 'items_provider.g.dart';
//
// @riverpod
// List<String> items(ItemsRef ref) {
// return [
// 'アニメ',
// 'グルメ',
// '不動産',
// 'ニュース',
// 'エンタメ',
// '教育',
// 'ビジネス',
// '自動車',
// 'バイク',
// '観光',
// '手芸',
// ];
// }
// 自動生成なしの場合
final itemsProvider = Provider<List<String>>((ref) {
return [
'アニメ',
'グルメ',
'不動産',
'ニュース',
'エンタメ',
'教育',
'ビジネス',
'自動車',
'バイク',
'観光',
'手芸',
];
});
// 自動生成なしの場合は必要
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 自動生成ありの場合は必要
// import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
const _key = 'favorites';
// ダミーデータ
// @riverpod
// Future<Set<String>> favorites (FavoritesRef ref) async {
// return {
// 'アニメ',
// 'グルメ',
// };
// }
// 自動生成ありの場合
// part 'favorites_provider.g.dart';
// @riverpod
// class Favorites extends _$Favorites {
// @override
// FutureOr<Set<String>> build() async {
// final sharedPreferences = await SharedPreferences.getInstance();
// final favorites = sharedPreferences.getStringList(_key) ?? <String>[];
// return Set.from(favorites);
// }
//
// Future<void> addItem(String item) async {
// final set = state.value ?? {};
// set.add(item);
// await _saveSet(set);
// }
//
// Future<void> removeItem(String item) async {
// final set = state.value ?? {};
// set.remove(item);
// await _saveSet(set);
// }
//
// Future<void> _saveSet(Set<String> set) async {
// final sharedPreferences = await SharedPreferences.getInstance();
// sharedPreferences.setStringList(_key, set.toList(growable: false));
// state = AsyncData(set);
// }
// }
// 自動生成なしの場合
final favoritesProvider = StateNotifierProvider<FavoritesNotifier, Set<String>>((ref) {
return FavoritesNotifier();
});
class FavoritesNotifier extends StateNotifier<Set<String>> {
FavoritesNotifier() : super({}) {
loadFavorites(); // 初期化時にお気に入りを読み込む
}
Future<void> loadFavorites() async {
final sharedPreferences = await SharedPreferences.getInstance();
final favorites = sharedPreferences.getStringList(_key) ?? <String>[];
state = Set.from(favorites);
}
Future<void> addItem(String item) async {
final set = Set<String>.from(state);
set.add(item);
await _saveSet(set);
state = set;
}
Future<void> removeItem(String item) async {
final set = Set<String>.from(state);
set.remove(item);
await _saveSet(set);
state = set;
}
Future<void> _saveSet(Set<String> set) async {
final sharedPreferences = await SharedPreferences.getInstance();
sharedPreferences.setStringList(_key, set.toList(growable: false));
state = set;
}
}
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'items/items_provider.dart';
import 'favorites/favorites_provider.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(itemsProvider);
final favorites = ref.watch(favoritesProvider);
return Scaffold(
appBar: AppBar(
title: const Text('あなたの興味は?'),
),
body: ListView.builder(
itemBuilder: (context, index) {
final item = items[index];
// 自動生成ありの場合
// final isFavorite = favorites.valueOrNull?.contains(item) ?? false;
// 自動生成なしの場合
final isFavorite = favorites.contains(item);
return ListTile(
leading: IconButton(
onPressed: () {
if (isFavorite) {
ref.read(favoritesProvider.notifier).removeItem(item);
} else {
ref.read(favoritesProvider.notifier).addItem(item);
}
},
icon: isFavorite
? const Icon(Icons.favorite, color: Colors.pink)
: const Icon(Icons.favorite_outline),
),
title: Text(item),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ItemDetailPage(item: item),
),
);
},
);
},
itemCount: items.length,
),
);
}
}
class ItemDetailPage extends ConsumerWidget {
const ItemDetailPage({required this.item, super.key});
final String item;
@override
Widget build(BuildContext context, WidgetRef ref) {
final favorites = ref.watch(favoritesProvider);
// 自動生成ありの場合
// final isFavorite = favorites.valueOrNull?.contains(item) ?? false;
// 自動生成なしの場合
final isFavorite = favorites.contains(item) ?? false;
return Scaffold(
appBar: AppBar(
title: Text('$item'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$itemの詳細画面',
style: TextStyle(fontSize: 24),
),
IconButton(
onPressed: () {
if (isFavorite) {
ref.read(favoritesProvider.notifier).removeItem(item);
} else {
ref.read(favoritesProvider.notifier).addItem(item);
}
},
icon: isFavorite
? const Icon(
Icons.favorite,
size: 50,
color: Colors.pink,
)
: const Icon(
Icons.favorite_outline,
size: 50,
))
],
),
),
);
}
}