Riverpodとは
RiverpodはFlutterにて状態管理を行うためのパッケージ。
同じく状態管理向けパッケージのproviderと同じ作者が作成したもので、 providerと比較して様々な改良がなされている。
使い方
インストール
dependencies: flutter_riverpod: ^2.4.0
providerをグローバルで宣言
グローバルにてfinalでproviderを作成する。
final myProvider = Provider((ref) { return [ 管理する状態 ] } );
providerには以下の種類があり、用途によって使い分ける。
- Provider
- キャッシュなどで使用する(サンプルのTodoアプリ)
- StateNotifierProvider
- 自作クラスなどを通知する際に使用する
- 自作クラスはStateNotifierを継承する
- FutureProvider
- StreamProvider
- StateProvider
- 外部から状態を変更したい場合使用
- 組み込み型(int, String, bool...)などの軽量な状態を管理するのに向いている
- 自作クラスなどを管理する場合、StateNotifierProviderの使用が推奨されている
- ChangeNotifierProvider
アプリ全体をProviderScopeでラップ
providerを伝播させるため、ProviderScopeでルートウィジェットをラップする。
void main() { runApp( const ProviderScope( child: [ ルートウィジェット ], ), ); }
従来のproviderだとMultiProvider
などでラップしてproviderを登録する必要があったが、
riverpodではproviderの登録は不要。
ConsumerWidgetなどを継承したクラスを作成
ConsumerWidget
などのウィジェットを継承してウィジェットを作成する。
class MyWidget extends ConsumerWidget { const MyWidget({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { ... // refを参照してproviderから状態を取得 } }
buildメソッド内でref
を参照して登録されているproviderから状態を取得できる。
通常のStatelessWidget
との違いはWidgetRef
を使用できる点のみ。
なので状態参照が不要なウィジェットはConsumerWidget
などを継承する必要はない。
providerを参照するためのウィジェットは以下がある。
- ConsumerWidget
- StatelessWidgetの代替
- ConsumerStatefulWidget+ConsumerState
- StatefulWidget+Stateの代替
- HookConsumerWidget
- HookWidget の代替
- flutter_hooksで使用する
- StatefulHookConsumerWidget
- HookWidgetの代替
- flutter_hooksで使用する
- Consumer
providerから状態を取得
ref.watch
providerの変更を監視するならばref.watch()
を使用する。
ref.watch([ 取得したいprovider名 ])
ref.listen
provider更新を監視し、更新タイミングで何かしらの処理を実行したい場合ref.listen()
を使用する。
ref.listen([ 取得したいprovider名 ], ([ 変更前provider ], [ 変更後provider ]){ [実行したい処理] })
ref.listen()
の使用ケースは、変更をユーザーに通知したい場合など。
例えばproviderで管理した値の変化をsnackbarやダイアログで表示するケース。
変更前と変更後を取得できるので、「〇〇を[変更前]→[変更後]に更新しました」などの文言が出せる。
ref.read
providerの監視が不要な場合、ref.read()
を使用する。
ref.read( [取得したいprovider名] )
公式ではあまり使用が推奨されていない様子。 使用ケースはproviderの値が不要でメソッドを実行したい場合など。
サンプル
カウンターアプリ
- StateProviderでint値を管理
- インクリメントとリセットボタンを用意
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // カウンター管理用provider final counterProvider = StateProvider<int>((_) => 0); void main() { runApp( // アプリをラップしてproviderを伝播させる const ProviderScope( child: MyApp(), ), ); } class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('Counter')), body: Center( child: Text( // カウンター値を表示 "${ref.watch(counterProvider)}", style: const TextStyle(fontSize: 48), ), ), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( child: const Icon(Icons.add), // stateをインクリメント onPressed: () => ref.watch(counterProvider.notifier).state++, ), const SizedBox( height: 20, ), FloatingActionButton( backgroundColor: Colors.red, child: const Icon( Icons.exposure_zero, color: Colors.white, ), // stateをリセット onPressed: () => ref.refresh(counterProvider), ), ], ), ), ); } }
Todoアプリ
- todoの追加、完了状態変更
- 完了状態のみ表示
- 完了todoのみを表示する場合はProviderでキャッシュ
リポジトリは以下。
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // provider -------------------------------- /// 全todo final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) { return TodosNotifier(); }); /// 完了済みtodo final completedTodosProvider = Provider<List<Todo>>((ref) { final todos = ref.watch(todosProvider); return todos.where((todo) => todo.isCompleted).toList(); }); // model -------------------------------- /// todoモデル class Todo { Todo(this.id, this.description, this.isCompleted); final int id; final bool isCompleted; final String description; Todo copyWith(int id, String description, bool isCompleted) { return Todo(id, description, isCompleted); } } // StateNotifier -------------------------------- /// todo通知用 class TodosNotifier extends StateNotifier<List<Todo>> { TodosNotifier() : super([]); /// 追加 void addTodo(String description) { int maxId = 0; for (Todo todo in state) { if (todo.id > maxId) { maxId = todo.id; } } final newTodo = Todo(maxId + 1, description, false); state = [...state, newTodo]; } /// 完了状態を更新 void toggle(int todoId) { state = [ for (Todo todo in state) todo.id == todoId ? todo.copyWith(todo.id, todo.description, !todo.isCompleted) : todo ]; } } // view -------------------------------- void main() { runApp( // アプリをラップしてproviderを伝播させる const ProviderScope( child: MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( routes: { "/": (context) => const TodoListPage(), "/create": (context) => const TodoCreatePage() }, ); } } /// todo一覧ページ class TodoListPage extends ConsumerStatefulWidget { const TodoListPage({super.key}); @override ConsumerState<TodoListPage> createState() => _TodoListPageState(); } class _TodoListPageState extends ConsumerState<TodoListPage> { bool isFiltered = false; @override Widget build(BuildContext context) { final todos = ref.watch(todosProvider); final completedTodos = ref.watch(completedTodosProvider); return Scaffold( appBar: AppBar( title: const Text('Todo List'), actions: [ IconButton( onPressed: () { Navigator.of(context).pushNamed("/create"); }, icon: const Icon(Icons.add)) ], ), body: Center( child: Column( children: [ Container( padding: EdgeInsets.all(16), // height: 80, width: double.infinity, child: Row(children: [ Row(children: [ Checkbox( value: isFiltered, onChanged: (value) { setState(() { isFiltered = value!; }); }), const Text("completed only"), ]) ]), ), isFiltered ? Expanded( child: ListView.builder( itemCount: completedTodos.length, itemBuilder: (context, index) { return Card( child: CheckboxListTile( title: Text(completedTodos[index].description), value: completedTodos[index].isCompleted, onChanged: (value) { ref .read(todosProvider.notifier) .toggle(todos[index].id); }, ), ); }), ) : Expanded( child: ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { return Card( child: CheckboxListTile( title: Text(todos[index].description), value: todos[index].isCompleted, onChanged: (value) { ref .read(todosProvider.notifier) .toggle(todos[index].id); }, ), ); }), ), ], )), ); } } /// todo作成ページ class TodoCreatePage extends ConsumerStatefulWidget { const TodoCreatePage({super.key}); @override ConsumerState<TodoCreatePage> createState() => _CreateTodoState(); } class _CreateTodoState extends ConsumerState<TodoCreatePage> { final _controller = TextEditingController(); @override void dispose() { super.dispose(); _controller.dispose(); } @override Widget build( BuildContext context, ) { return Scaffold( appBar: AppBar( title: const Text("Create Todo"), automaticallyImplyLeading: false, ), body: Column( children: [ Container( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("Description"), TextField( controller: _controller, ), ], ), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () => Navigator.of(context).pop(), child: const Text("Cancel"), style: ElevatedButton.styleFrom(backgroundColor: Colors.grey), ), const SizedBox( width: 10, ), ElevatedButton( onPressed: () { if (_controller.text.isNotEmpty) { ref .read(todosProvider.notifier) .addTodo(_controller.text); Navigator.of(context).pop(); } }, child: const Text("Create")), ], ) ], ), ); } }