はじめに
Flutterを網羅的に学習するにあたってRoadmapを使って学習を進めることにしました。
この記事では、Flutter初学者やこれからFlutterを学習し始める方に向けて、Riverpodについてまとめています。
RoadmapはFlutterだけでなく、他の言語やスキルのロードマップも提供されており、何から学習して良いか分からないと悩んでいる方にとって有用なサイトになっています。
ぜひRoadmapを利用して学習してみてください。
Roadmapとは
簡潔に言えば、Roadmap.shは学習者にとってのガイドブックであり、学習の方向性を提供する学習ロードマップサイトです。
初心者から上級者まで、ステップバイステップでスキルを習得するための情報が提供されています。
学習の進め方が分かりやすく示されているだけでなく、個々の項目に参考資料やリソースへのリンクも提供されているので、学習者は目標を設定し、自分自身のペースで学習を進めることができます。
Riverpod
FlutterロードマップRiverpodでは以下の5つのサイトが紹介されています。興味のある方はぜひお読みください。
- riverpod: https://pub.dev/packages/riverpod
- Riverpod in Flutter: https://docs.flutter.dev/data-and-backend/state-mgmt/options#riverpod
- Documentation: https://riverpod.dev/
- Documentation v2 is in progress: https://docs-v2.riverpod.dev/
- Flutter Riverpod 2.0: The Ultimate Guide: https://codewithandrea.com/articles/flutter-state-management-riverpod/
Riverpodとは
Riverpodは、Flutterアプリケーションのための状態管理ライブラリで、Provider パッケージのようにウィジェットツリー内でデータを提供するために使用されます。Riverpodは、Provider パッケージを改良し、柔軟性と拡張性を向上させたもので、複雑な状態を簡単に管理することができるライブラリです。
Riverpodを使用することで、任意の Widget にデータを受け渡すことが出来るようになります。
Riverpodの特徴
-
スコープの明確化: Riverpodでは、プロバイダーのスコープがより厳密に定義されてい流ため、Providerごとに異なるスコープを持たせることができ、依存関係をより制御しやすくしています。
-
リアクティブなデータ変更: リアクティブな変更通知を提供しており、データが変更された際に関連するウィジェットが自動的に再構築される仕組みをサポートしています。
-
様々なProvider: Riverpodでは、
Provider
だけでなく、FutureProvider
、StreamProvider
など、異なるデータソースに対するプロバイダーが統一的なAPIで提供されています。 -
依存関係の設定: 依存関係を自動的に解決し、プロバイダー間の依存関係を簡単に設定できるようになっています。
-
Null Safety:
Null Safety
をサポートしており、安全性と安定性が向上しています。 -
利用範囲の広さ: 小規模なアプリケーションから大規模なアプリケーションまで幅広く利用できます。プロバイダーのスコープの明確さと柔軟性により、アプリケーションのサイズに合わせて効果的に使用できます。
-
リアクティブなUIの構築: リアクティブな変更通知をサポートしているため、データの変更に応じてUIが自動的に更新されるようなアプリケーションに適しています。
-
非同期データの処理:
FutureProvider
やStreamProvider
を使用して非同期データを処理し、リアルタイムなデータの表示や取得が必要な場合に有用です。 -
テスト容易性: グローバル変数として定義するため、アクセスが保証されるのと同時にテスト容易性も担保されます。複雑な setUp / tearDown ステップが手順が不要になり、Providerを上書きできるので特定の動作テストが簡単です。
-
パフォーマンスの向上: 「状態」を別の「状態」と組み合わせることも簡単に行え、状態の変化によって影響を受けるもののみが再計算されるのでWidgetの再構築が最適化されます。
Providerとの違い
Providerパッケージは InheritedWidget
を改良する形で開発されたパッケージでWidgetツリーに依存しています。親のWidgetツリーを購読して、登録されている Provider
にアクセスするため、親のWidgetツリーには使用したい Provider
が登録されている必要があります。
もし Provider
が登録がされていない場合は ProviderNotFoundException
エラーが発生してしまいます。
一方でRiverpodパッケージは、 Provider
をWidgetツリーから切り離してグローバルに定義する事が出来るため、定義した Provider
に確実にアクセスする事が出来ます
また、Providerパッケージでは同じ型のものが複数同時に使用できないのに対して、Riverpodパッケージでは同じ型の Provider
を複数参照できるため、Providerパッケージの欠点を補ってくれています。
Riverpodパッケージ
Riverpodのパッケージには3種類あります。
パッケージ名 | 形態 | 説明 |
---|---|---|
flutter_riverpod | Flutterのみ | RiverpodをFlutterと統合するためのパッケージです。Flutterアプリケーション内での Riverpod の利用においては、通常このパッケージを使用します。 |
hooks_riverpod | Flutter + flutter_hooks | ReactのHooks APIのような、ウィジェットを構築するためのフック(Hooks)を提供します。再利用可能で理解しやすいコードを書くことができるため、Riverpod をよりシンプルに使用するための手段として使用されます。 |
riverpod | Dart のみ | Flutterに関連するすべてのクラスが削除された、Dartのみのバージョンです。Flutter機能を使用しないパッケージではこちらを使用することになります。 |
使い方
1. ProviderScopeをルートに追加
Riverpodを使った Provider
の宣言自体はグローバルですが、 Provider
をアプリ内で利用するために範囲(スコープ)を指定する必要があります。アプリのルート部分を ProviderScope
でラップすることで、下位ツリーのWidgetで Provider
を呼び出すことができるようになります。。
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
2. Providerをグローバル変数として定義
プロバイダの作成方法には手動でコードを設定する方法とコード生成を使用する方法の2種類があります。
手動で設定
Provider
の引数に指定したコールバック関数で return
するものが管理するデータになります。コールバック関数の引数の ref
は他のProviderのデータを取得したい時に使用します。Providerのデータを取得していない場合は、 ref
ではなく _
として問題ありません。以下は手動設定の場合のサンプルコードです。
final myProvider = Provider<MyApp>((ref) {
return MyApp();
});
// アロー構文を使用した場合
final myProvider = Provider((ref) => MyApp());
-
final myProvider
は、定数の宣言です。定数名には使用するProvider
の名前を定義します。 -
Provider<T>
で Riverpod で使うProvider
の種類を設定しています。 -
<MyApp>
は型の宣言を行っています。このProvider
はMyApp
という型を返すということを明示的に示しています。 -
((ref) { return MyApp(); });
では、ref
という引数を受け取り、MyApp()
という値を返しています。ref
を使用しない場合は((_) { return MyApp(); });
のようにして省略することも可能です。 -
ref
はRiverpod
のRefオブジェクトを表します。Refを使うことで他のプロバイダーにアクセスしたり、状態の変更を通知したりできます。以下はref
の使用例です。 -
他のプロバイダーを読み込む
final user = ref.read(userProvider);
- StateNotifierの変更を通知する
ref.read(countProvider.notifier).update((state) => state + 1);
- Riverpodで提供されているメソッドを利用する
ref.listen<String>(helloProvider, (prev, next) {
// 状態変更時の処理
});
コード生成
part 'my_provider.g.dart';
@riverpod
MyApp my(MyRef ref) {
return MyApp();
}
-
コード生成を使用するので、
part: '{file_name}.g.dart';
の宣言が必須です。 -
@riverpod
アノテーションをつけて、プロバイダ生成のためのメソッドであることを示します。 -
メソッド先頭の
MyApp
は、関数の戻り値の型を指定しています。 -
関数名である
my
はプロバイダの名前になります。設定で変更することもできますが、コード生成を行うと末尾にProvider
が自動で付くので、myProvider
という名前になります。 -
関数のパラメータである
(MyRef ref)
はコード生成されるMyRef
を関数内で使用するためのものです。コード生成前はコンパイルエラーになりますが、コード生成を実行すれば生成されるものなので問題ありません。
3. Providerからデータを取得する
Provider
からデータを取得するためには ConsumerWidget
を使用します。 ConsumerWidget
を継承した Widget を定義すると、 build()
に WidgetRef
型の引数が追加されます。 追加された WidgetRef
型の引数に watch()
または read()
のメソッドを使用して、 Provider
を指定することで Provider
が管理しているデータを取得することができます。
データの変更を監視する必要がある時は watch()
を、監視する必要がない時は read()
を使用してデータを取得します。以下のコードはProviderからデータを取得するカウンタアプリのサンプルコードです。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CountNotifier extends StateNotifier<int> {
CountNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
final countProvider =
StateNotifierProvider<CountNotifier, int>((ref) => CountNotifier());
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Riverpod Counter',
home: MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(countProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Riverpod Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Count',
style: TextStyle(fontSize: 32),
),
Text(
'$count',
style: const TextStyle(fontSize: 40),
),
],
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
ref.read(countProvider.notifier).increment();
},
child: const Icon(Icons.add),
),
const SizedBox(width: 8),
FloatingActionButton(
onPressed: () {
ref.read(countProvider.notifier).decrement();
},
child: const Icon(Icons.remove),
),
],
),
);
}
}
-
CountNotifier:
-
CountNotifier
はStateNotifier
を継承し、整数の状態を管理するクラスです。 - コンストラクタで初期値 0 を指定しています。
-
increment
は状態を増やし、decrement
は状態を減らすメソッドです。
-
-
countProvider:
-
StateNotifierProvider
を使用して、CountNotifier
のインスタンスを提供するプロバイダーを作成しています。このプロバイダーはCountNotifier
の状態を取得するために使用されます。
-
-
MyHomePage:
-
ConsumerWidget
を継承し、プロバイダーの状態を監視するためのウィジェットです。ref.watch(countProvider)
でcountProvider
の状態を取得しています。また、ref.read(countProvider.notifier)
でCountNotifier
のメソッドにアクセスしています。 - ボタンが押されると、
increment
とdecrement
それぞれのメソッドが呼び出され、状態が変更されます。 -
final count = ref.watch(countProvider);
で状態を監視しているため、ボタンが押されて状態が変更されるたびに、Textが更新されていきます。
-
主なProvider
名前 | 概要 |
---|---|
Provider | 簡単な状態を提供する最も基本的なプロバイダであり、値を同期的に生成する |
StateProvider | 外部から変更が可能な状態を公開するプロバイダ、ステートの管理にわざわざ StateNotifier クラスを定義するほどではない場合に使用する。 |
StateNotifierProvider | StateNotifierを監視し、公開するためのプロバイダ |
ChangeNotifierProvider | ChangeNotifier を Flutter で利用するためのプロバイダ、Riverpod では使用を非推奨 |
FutureProvider | Futureをラップした非同期操作可能なProvider 非同期で取得した値を提供する |
StreamProvider | Streamをラップした非同期操作可能なProvider 断続的に最新の値を提供する |
Provider
外部から変更することができないデータを管理する基本的な Provider です。状態の変更通知などは自動的に行われず、単なるデータの提供に焦点が当てられています。
計算結果をキャッシュしたり、変更することのないデータを渡す際などに使用されます。キャッシュの例でいくと、フィルタリング機能が挙げられます、データにフィルタを適用する場合、画面が再描画される度にフィルタを適用すると余計な処理が入ってしまいます。そこで、 Provider
を利用することで、フィルタリングしたデータをキャッシュさせることができ、都度処理が呼ばれるのを防ぐことができるのです。
サンプルコード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Riverpod Counter',
home: MyHomePage(),
);
}
}
final strProvider = Provider((ref) {
return 'Hello Riverpod';
});
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final value = ref.watch(strProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Riverpod Sample'),
),
body: Center(
child: Text(
value,
style: const TextStyle(
fontSize: 24
),
),
),
);
}
}
-
strProvider
は、Riverpod
のProvider
を使用して定義されています。このプロバイダーは文字列 'Hello Riverpod' を返しています。 -
MyHomePage
クラスは、ConsumerWidget
を継承し、build
メソッドでUIを構築しています。strProvider
の値をref.watch
で取得し、それをテキスト表示しています。
StateProvider
外部から変更することができるデータを管理するProviderです。StateNotifierProvider の簡易版で、ステートの管理に StateNotifier
クラスを定義するほどではないシンプルなステートを管理する場合に使用します。
外部からデータを変更する時は StateController
インスタンスの update()
、または直接 state
に変更を加えることでデータを変更します。
シンプルなステートを管理する場合
- 列挙型(enum)、フィルタの種類など
- 文字列型、テキストフィールドの入力内容など
- bool 型、チェックボックスの値など
- 数値型、ページ数やフォームの年齢など
StateProviderを使用すべきではないステート
- ステートの算出に何かしらのバリデーション(検証)ロジックが必要な場合
- ステート自体が複雑なオブジェクトである場合(カスタムのクラスや List/Map など)
上記のような複雑なステートを管理する場合は、 StateNotifierProvider
を使用することが推奨されています。
サンプルコード
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class Product {
Product({required this.name, required this.price});
final String name;
final double price;
}
final _products = [
Product(name: 'iPhone', price: 999),
Product(name: 'cookie', price: 2),
Product(name: 'ps5', price: 500),
];
enum ProductSortType {
name,
price,
}
final productSortTypeProvider = StateProvider<ProductSortType>(
(ref) => ProductSortType.name,
);
final productsProvider = Provider<List<Product>>((ref) {
final sortType = ref.watch(productSortTypeProvider);
switch (sortType) {
case ProductSortType.name:
return _products.sorted((a, b) => a.name.compareTo(b.name));
case ProductSortType.price:
return _products.sorted((a, b) => a.price.compareTo(b.price));
}
});
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ref.watch(productSortTypeProvider),
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(
Icons.sort_by_alpha,
color: Colors.black,
),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(
Icons.sort,
color: Colors.black,
),
),
],
),
],
),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}
}
-
Product クラス:
-
Product
クラスは商品を表すデータモデルです。商品には名前(name)と価格(price)が含まれています。
-
-
_products リスト:
-
_products
リストには、Product
クラスのインスタンスが複数含まれています。これはサンプルの商品データです。
-
-
ProductSortType:
-
ProductSortType
は、商品のソート方法を示すために使用されます。name
の場合は名前でソートし、price
の場合は価格でソートします。
-
-
productSortTypeProviderプロバイダー:
-
productSortTypeProvider
は、商品をソートする方法(名前か価格か)を監視します。
-
-
productsProvider プロバイダー:
-
productsProvider
は、現在のソート方法に基づいて商品リストを返しています。
-
-
MyHomePage ウィジェット:
-
MyHomePage
クラスはConsumerWidget
を継承し、build
メソッドでUIを構築しています。商品リストが表示され、AppBarには商品をソートするドロップダウンメニューがあります。 - 商品リストは
ListView.builder
を使用して構築され、各商品はListTile
で表示されます。
-
-
ドロップダウンメニュー
onChanged
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
-
read
メソッドで、 プロバイダーの値を読み取り、productSortTypeProvider.notifier
で、StateProvider
の更新メソッドにアクセスし、state = value!
で状態を更新しています。
上記のように state
を使用して新たなステートを作成することもできますが、 現在の状態をパラメータとして利用できること、状態のイミュータブルな更新が保証されることから update
メソッドを使うことが推奨されています。
上記のコードを update
メソッドを使用して書くと以下のようになります。
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).update(
(state) => state = value!,
),
StateNotifierProvider
StateNotifier
を監視し、データとして管理するためのプロバイダです。変化するステートを管理するソリューションとして推奨されています。
一般的には以下のような場面で使用されます。
- イミュータブル(不変)なステートを管理する場合 (イミュータブルではあるが、イベントに応じて状態が変わることがある場合)。
- ステートを変更するためのロジックを一つの場所で集中管理する場合。
以下のサンプルコードは、 StateNotifierProvider
を使用して Todo リストを作成しています。 StateNotifierProvider
を使うことで addTodo
などのメソッドをUI 側に渡して Todo リストの内容を操作できるようになります。
サンプルコード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'main.freezed.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
@freezed
class Todo with _$Todo {
factory Todo({
required String text,
}) = _Todo;
}
class TodoListNotifier extends StateNotifier<List<Todo>> {
TodoListNotifier() : super([]);
void addTodo(String text) {
state = [...state, Todo(text: text)];
}
void removeTodo(int index) {
state = List.from(state)..removeAt(index);
}
}
final todoListProvider = StateNotifierProvider<TodoListNotifier, List<Todo>>(
(ref) => TodoListNotifier(),
);
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo List',
home: MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
MyHomePage({Key? key}) : super(key: key);
final TextEditingController _textController = TextEditingController();
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(todoListProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Todo List'),
),
body: ListView.builder(
itemCount: state.length,
itemBuilder: (context, index) {
final todo = state[index];
return ListTile(
title: Text(todo.text),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
ref.read(todoListProvider.notifier).removeTodo(index);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddTodoDialog(context, ref),
child: const Icon(Icons.add),
),
);
}
Future<void> _showAddTodoDialog(BuildContext context, WidgetRef ref) async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Add Todo'),
content: TextField(
controller: _textController,
decoration: const InputDecoration(
hintText: 'Enter your todo',
),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
_textController.clear();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
final newText = _textController.text.trim();
if (newText.isNotEmpty) {
ref.read(todoListProvider.notifier).addTodo(newText);
}
Navigator.of(context).pop();
_textController.clear();
},
child: const Text('Add'),
),
],
);
},
);
}
}
-
Todoクラス:
- Todo クラスはFreezedパッケージによって生成された不変のデータクラスです。
@freezed
アノテーションを使用して Freezed クラスとして宣言されています。 -
_$Todo
は、Freezedが生成するファクトリーコンストラクタの名前です。_$Todo
の$はアンダースコアに続く文字を大文字に変換するFreezedの慣習です。 -
factory
キーワードを使ってコンストラクタを定義しており、required
でtext
パラメータを受け取るTodo()
コンストラクタが作成されています。 -
_Todo
クラスは Freezed によって生成される、不変のデータクラスです。このクラスのインスタンスは一度作成されたら変更できません。これにより、不変性が確保され、安全性が向上します。
- Todo クラスはFreezedパッケージによって生成された不変のデータクラスです。
-
TodoListNotifierクラス:
-
TodoListNotifier
クラスはStateNotifier
を継承しており、List<Todo>
の状態を管理しています。 -
state
プロパティはStateNotifier
クラスが管理する現在の状態を表しており、state =
で、新しい状態を指定することができます。 -
addTodo
メソッドの[...state, Todo(text: text)]
は、スプレッド演算子 (...) を使用して、現在の状態 (state) の要素を新しいリストに展開し、その後 Todo オブジェクトを追加しています。 -
removeTodo
メソッドのList.from(state)
は、現在の状態を新しいリストに複製しています。この複製を作成することで、元のリストに変更が加えられても元のリスト自体は変更されないため、不変性が保たれます。 また、..removeAt(index)
で、複製されたリストから指定された index の位置にある要素を削除しています。このカスケード演算子 (..) は、同じオブジェクトに対して複数の操作を行うために使用されます。
-
-
todoListProvider:
-
StateNotifierProvider
では、TodoListNotifier
を使用して List の状態を提供しているため、アプリケーション内の異なる部分で同じTodoリストを共有することができます。 -
StateNotifierProvider
のジェネリクス型引数は、2つの型引数を取ります。第一引数 (TodoListNotifier) は、状態を管理するStateNotifier
の型です。第二引数 (List) は、StateNotifier
が管理する状態の初期値の型です。 -
(ref) => TodoListNotifier()
は、Providerが初めて読み込まれたときに呼び出される初期化関数です。
-
-
MyHomePageウィジェット:
-
MyHomePage
クラスはConsumerWidget
を継承し、ウィジェットツリー内でtodoListProvider
を監視しています。 -
ref.watch
とref.read
を使用して、Todoリストの表示やTodoの追加・削除の機能を実装しています。
-
ChangeNotifierProvider
ChangeNotifierProvider は ChangeNotifier
を Flutter で利用するためのプロバイダです。Riverpod では以下の理由により、使用を非推奨にしています。可能な限り StateNotifierProvider
を使用し、 ChangeNotifierProvider
の使用はミュータブルなステート管理を行う必然性がある場合に限定してください。
-
package:provider
でChangeNotifierProvider
を利用していた際の移行作業を容易にするため - ミュータブル(可変)なステート管理をサポートするため
データの更新を監視元に伝えたい時は、 notifyListeners()
を実行する必要があります。notifyListeners()
を記載していないと、データを更新しても監視元に伝わらないので注意してください。
サンプルコード
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Todo List',
home: MyHomePage(),
);
}
}
class Counter extends ChangeNotifier {
int counter = 0;
void increment() {
counter++;
notifyListeners();
}
}
final counterProvider = ChangeNotifierProvider((ref) {
return Counter();
});
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Riverpod Sample'),
),
body: Center(
child: Text(
state.counter.toString(),
style: const TextStyle(fontSize: 24),
),
),
floatingActionButton: FloatingActionButton(
onPressed: ref.read(counterProvider).increment,
child: const Icon(Icons.add),
),
);
}
}
-
Counterクラス:
-
ChangeNotifier
を継承したCounter
クラスがあります。Counter
クラスは、カウンターの値を保持し、increment
メソッドを使用して値を増やし、変更があった場合にリスナーに通知します。
-
-
ChangeNotifierProvider:
-
ChangeNotifierProvider
は、Counter
クラスのプロバイダーを作成しています。これにより、Counter
クラスの状態が変更されたときにウィジェットツリーがリビルドされるようになります。
-
-
MyHomePageウィジェット:
-
MyHomePage
はConsumerWidget
を継承しており、ref.watch(counterProvider)
を使用してCounter
の状態を監視し、ref.read(counterProvider)
を使用してCounter
のメソッドを呼び出しています。 - カウンターの値を表示するTextウィジェットがあります。この値は
state.counter
から取得され、Counter
の状態に変更があると自動的に更新されます。 -
FloatingActionButton
をタップすると、controller.increment
が呼び出されてカウンターが増加し、UIが更新されます。
-
FutureProvider
FutureProvider は非同期操作が可能な Provider で、 Future
から取得したデータを管理する際に使用します。
主に以下のような用途で使われます。
- 非同期操作を実行し、その結果をキャッシュするため。(ネットワークリクエストなど)
- 非同期操作の error状態、loading状態を適切に処理するため。
- 非同期的に取得した複数の値を組み合わせて一つの値にするため。
FutureProvider は ref.watch
を使用することで、何かの値が変わったときに自動でデータを再取得するよう設定でき、プロバイダが常に最新データを渡すことを保証できます。
サンプルコード
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Future<String> fetchData() async {
await Future.delayed(const Duration(seconds: 2));
return 'Hello, Riverpod!';
}
final futureProvider = FutureProvider<String>((ref) => fetchData());
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'FutureProvider Sample',
home: MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<String> data = ref.watch(futureProvider);
return Scaffold(
appBar: AppBar(
title: const Text('FutureProvider Sample'),
),
body: Center(
child: data.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
data: (value) => Text(
value,
style: const TextStyle(fontSize: 24),
),
),
),
);
}
}
-
fetchData関数:
-
fetchData
メソッドは非同期で2秒待機した後、'Hello, Riverpod!'という文字列を返す関数です。
-
-
FutureProvider:
-
FutureProvider
は非同期データを提供するためのProviderです。この場合、Future<String>
型の非同期関数であるfetchData
を使用して非同期データを提供しています。
-
-
MyHomePageクラス:
-
MyHomePage
はConsumerWidget
を継承しています。 -
ref.watch(futureProvider)
を使用して非同期データを取得し、AsyncValue
のwhenメソッドを使用して非同期データの状態に応じてUIを構築しています。ローディング中はCircularProgressIndicator
が表示され、エラーが発生した場合はエラーメッセージが表示され、データが利用可能な場合はそのデータが表示されます。 -
when()
のdata 引数には非同期処理が完了した時に表示するウィジェットを返すコールバック関数、loading 引数には非同期処理を実行している時に表示するウィジェットを返すコールバック関数、error 引数には非同期処理に失敗した時に表示するウィジェットを返すコールバック関数をそれぞれ指定します。 - error: エラーの詳細情報が含まれるオブジェクトです。通常、これはException型のオブジェクトですが、FutureProviderが提供するエラーには他にも様々な型があります。エラーが何であるかは非同期データを提供しているプロバイダーに依存します。例えば、通信エラーの場合はDioErrorなど。
- stack: エラーが発生したときのスタックトレースが含まれるオブジェクトです。通常、デバッグやエラーのトラブルシューティングに使用されます。
-
StreamProvider
StreamProvider は FutureProvider の Stream 版です。
一般的には以下のような用途で使われます。
- Firebase や WebSocket の監視するため。
- 一定時間ごとに別のプロバイダを更新するため。
Stream 自体が値の更新を監視する性質を持つため、 StreamBuilder
で十分ではないかと思うかもしれません。しかし、 StreamBuilder
の代わりに StreamProvider
を使用することで以下の利点を得ることができます。
- 他のプロバイダで
ref.watch
を通じてストリームを監視することができる。 - AsyncValue により loading/error のステートを適切に処理することができる。
- ストリームから出力された直近の値をキャッシュしてくれる。(途中で監視を開始しても最新の値を取得することができる)
- StreamProvider をオーバーライドすることでテスト時のストリームを簡単にモックすることができる。
サンプルコード
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Stream<String> fetchData() {
return Stream.periodic(const Duration(seconds: 1), (value) {
return 'Data $value from stream';
});
}
final streamProvider = StreamProvider<String>((ref) => fetchData());
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'StreamProvider Sample',
home: MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<String> data = ref.watch(streamProvider);
return Scaffold(
appBar: AppBar(
title: const Text('StreamProvider Sample'),
),
body: Center(
child: data.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
data: (value) => Text(
value,
style: const TextStyle(fontSize: 24),
),
),
),
);
}
}
-
fetchData関数:
-
fetchData
関数は、1秒ごとに新しいデータを生成する非同期なストリームを作成しています。 -
Stream.periodic
は、一定の時間間隔で定期的に値を生成するストリームを作成するメソッドです。 -
const Duration(seconds: 1)
で1秒ごとに新しい値を生成するように指定しています。
-
-
StreamProviderの定義:
-
StreamProvider
は非同期なストリームからデータを提供するための Provider です。この場合、fetchData
関数を使用して非同期ストリームを提供しています。
-
-
MyHomePageクラス:
-
ConsumerWidget
を継承し、StreamProviderが提供する非同期なストリームをref.watch(streamProvider)
を使用して取得し、 AsyncValue のwhenメソッドを使用して非同期データの状態に応じてUIを構築しています。 - データが読み込み中なら
CircularProgressIndicator
、エラーが発生したらエラーメッセージ、データが利用可能ならそのデータが表示されます。
-
Provider修飾子とメソッド
family
外部からパラメータを渡してデータを作成するようにしてくれる修飾子です。
作成したプロバイダに .family
修飾子を付けると、パラメータが追加され、追加されたパラメータはプロバイダのステート(状態)を計算する要素として使用することができます。
以下は.family
修飾子を使用する場面の例です。
- FutureProvider と family を組み合わせて、メッセージ ID から Message を取得する場合
- プロバイダにアプリの現在のロケール情報を渡して、翻訳文を取得する場合
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
return dio.get('http://my_api.dev/messages/$id');
});
上記の family プロバイダをウィジェットなどで使用する場合、構文は通常のものと若干異なります。次の構文ではコンパイル時エラーが発生してしまいます。
Widget build(BuildContext context, WidgetRef ref) {
// エラー – messagesFamily はプロバイダではありません
final response = ref.watch(messagesFamily);
}
代わりに、次のように messagesFamily
に引数を渡してください。
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(messagesFamily('id'));
}
.family
修飾子を使用するプロバイダに、異なる引数を同時に渡すことも可能です。例えば以下のように messagesFamily
プロバイダに異なるid情報を渡し、それぞれのresponseを取得することもできます。
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(messagesFamily('id'));
final response1 = ref.watch(messagesFamily('id1'));
}
.family
修飾子はプロバイダに複数のオブジェクトを渡すことができません。
しかし、プリミティブ型(bool/int/double/String)か、定数(プロバイダ)もしくは ==
と hashCode をオーバーライドしたイミュータブル(不変)オブジェクトのいずれかである限りは どのような オブジェクトでも渡すことができます。以下はサンプルコードです。
サンプルコード
@freezed
abstract class MyParameter with _$MyParameter {
factory MyParameter({
required int userId,
required Locale locale,
}) = _MyParameter;
}
final exampleProvider = Provider.autoDispose.family<Something, MyParameter>((ref, myParameter) {
print(myParameter.userId);
print(myParameter.locale);
});
@override
Widget build(BuildContext context, WidgetRef ref) {
int userId; // ユーザ ID をどこかで取得する
final locale = Localizations.localeOf(context);
final something = ref.watch(
exampleProvider(MyParameter(userId: userId, locale: locale)),
);
...
}
autoDispose
参照されなくなった Provider のデータを破棄してくれる修飾子です。
Firebase 使用時に、サービスとの接続を切って不必要な負荷を避けたり、ユーザが別の画面に遷移してまた戻って来る際に、ステートをリセットしてデータ取得をやり直す際などに使用されます。
以下のコードは .autoDispose
の使用例です。 .autoDispose
修飾子は他の修飾子と組み合わせることもできます。
final userProvider = StreamProvider.autoDispose.family<User, String>((ref, id) {
...
});
ref.keepAlive
プロバイダに .autoDispose
修飾子を付けると、 ref
オブジェクトに keepAlive
というメソッドが追加されます。
keepAlive
メソッドを実行することで、プロバイダが参照されなくなった際にもステートを維持するように設定することができます。
例えば、以下のように HTTP リクエスト完了後に keepAlive
を実行するとします。
final myProvider = FutureProvider.autoDispose((ref) async {
final response = await httpClient.get(...);
ref.keepAlive();
return response;
});
すると、リクエスト完了前に画面を破棄して再度同じ画面に戻った場合は HTTP リクエストが再実行される一方、 リクエスト完了後に同じ動作を行った場合はステートが維持されるため、HTTP リクエストが再実行されることはありません。
ref.onDispose
ref.onDispose
を使用すると、プロバイダが参照されなくなったタイミング(プロバイダのステートを監視するオブジェクトがなくなったタイミング)で 処理を追加することができます。以下はリクエスト完了前に画面を離れてステートが破棄されたら、HTTP リクエストをキャンセルするサンプルコードです。
final myProvider = FutureProvider.autoDispose((ref) async {
// http リクエストのキャンセルを実行するための package:dio のオブジェクト
final cancelToken = CancelToken();
// プロバイダのステートが破棄されたら http リクエストをキャンセル
ref.onDispose(() => cancelToken.cancel());
// データを取得しつつキャンセル用の `cancelToken` を渡す
final response = await dio.get('path', cancelToken: cancelToken);
// リクエストが成功したらステートを維持する
ref.keepAlive();
return response;
});
Consumer と HookConsumer ウィジェット
ref オブジェクトは Consumer
もしくは HookConsumer
ウィジェットのコールバック関数 builder
から取得することもできます。
これらのウィジェットを使用する場合は ConsumerWidget
または HookConsumerWidget
のようにクラスを定義する必要がありません。以下は HookConsumer
を使用した例になります。
Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
final state = useState(0);
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
);
ref.select
プロバイダを監視する対象を限定したい場合に使用します。例えば、特定の状態しか更新しない場合などに使用することで、不要な更新を避けることができます。
プロバイダを監視するということは、そのプロバイダが公開するオブジェクト全体の状態を監視するということです。 しかし、その監視の範囲を狭めて特定のプロパティのみを監視対象としたい場合があります。
例えば、以下のような User
オブジェクトを公開するプロバイダがあるとします。
abstract class User {
String get name;
int get age;
}
それを監視するウィジェット側は user
の name
プロパティしか使用しません。
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}
上記のように userProvider
を普通に監視すると、 無関係な age
プロパティの変化もウィジェット更新のトリガーとなってしまいます。監視対象を name
プロパティに限定したい場合は、 select
を使用してその旨を明示的に宣言します。変更後のコードは以下のようになります。
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
ref.read
取得時点での状態を取得するのに使用します。状態の監視は行いません。
onTap
や initState
などのイベント処理でメソッドを使用する際などに使われます。
ref.read
は build
メソッドで使用しない
ウィジェットのパフォーマンスを最適化するため、 ref.read
を以下のように使うことがあるかもしれません。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
しかし上記のようなコードは追跡困難なバグの要因になり得ます。プロバイダが公開する値は変わることがないため、 ref.read
の使用は安全であると思うかもしれませんが、 常にプロバイダの値が変わらないということは保証できません。
変わらないと考えられていた値が、変更された場合、値の取得に ref.read
を使っていた部分をすべて ref.watch
に変更する必要があります。これはエラー発生の原因になり、変更し忘れる箇所がいくつか出てくる可能性もあります。
一方、最初から ref.watch
を使用していれば、リファクタリング時に生じる問題は比較的少なくなるため、ほとんどの場面では watch
や listen
の使用、特に watch
の使用がベターだと考えられています。
ウィジェットの更新回数を抑えるために ref.read
を使いたい場合であっても、 ref.watch
を使用してまったく同じ効果を得ることが可能です。
例えば
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
とする代わりに、
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
としてください。 この2つのコードの効果はいずれも同じであり、カウンターの数字が増えた際にボタンが更新されることはありません。加えて、後者のアプローチの場合はカウンターの数字がリセットされた場合にも対応することができます。
例えば、以下のコードように別の場所で ref.refresh
メソッドを使用して StateController
オブジェクトを再度生成するとします。
もしここで ref.read
を使用していた場合、ボタンは古い StateController
インスタンスを使うことになり、このインスタンスはこの時点で既に破棄されているため、使うべきではないはずです。 一方 ref.watch
を使用した場合は、ボタンが更新されて新しい StateController
を取得することができます。
ref.refresh(counterProvider);
ref.watch
状態を取得し、監視を続けるために使用します。値に変更があれば、画面更新が行われるため、Widgetにリアクティブな値を埋め込む場合などに使用します。
ref.listen
ref.watch
と同様にプロバイダを監視することができます。
最も大きな違いは、 ref.watch
が値の変化に応じてウィジェットやプロバイダを更新するのに対して、 ref.listen
は任意の関数を呼び出すことができます。エラー発生時のスナックバー表示など、何かしらの変化に反応して処理を実行したいときに使用します。
ref.listen
メソッドは第1引数にプロバイダ、第2引数にステート(状態)が変化した際に実行するコールバック関数を設定します。 このコールバック関数には呼び出し時に、プロバイダの直前のステートと新しいステートの値が渡されるため、それぞれをパラメータとして使用できます。以下は listen メソッドを使用したサンプルコードです。
final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
return Container();
}
}
まとめ
Riverpodとは
RiverpodはFlutterアプリケーションのための状態管理ライブラリで、柔軟性と拡張性が向上したProviderパッケージの改良版です。グローバルに定義されたプロバイダーを使用して、ウィジェットツリー内でデータを渡します。
基本的な使い方
1. ProviderScopeをルートに追加:
Riverpodを使用する際は、まず初めに ProviderScope をアプリのルートで追加します。
2. Providerをグローバル変数として定義:
Riverpodのプロバイダーはグローバルに定義され、手動で定義するかコード生成を使用して作成します。
3. Providerからデータを取得する:
ConsumerWidget を使用して、 build()
メソッド内で WidgetRef
型の引数を利用してプロバイダーからデータを取得します。
便利な機能
-
スコープの明確化:
Riverpodはプロバイダーのスコープを厳密に定義し、異なるスコープを持たせることができるため、依存関係の制御が容易になっています。 -
リアクティブなデータ変更:
リアクティブな変更通知をサポートしているため、データが変更された際に関連するウィジェットを自動的に再構築することができます。 -
様々なProvider:
Providerだけでなく、FutureProvider、StreamProviderなど異なるデータソースに対する様々なプロバイダーが提供されています。 -
Null Safety:
Null Safetyをサポートしているため、安全性と安定性が確保されています。
参考資料