現時点でのLatest:2.6.1で動作するRiverpodのサンプルソースです。
Flutter Riverpodの要点
用語説明
ConsumerWidget: 外部の「状態(State)」を観察することが出来るWidget。ref経由でProviderの中にあるStateを覗くことができる。
原則としてref.watch()メソッドを使う。(特別な場合のみ、ref.listen() , ref.read()を使う)
State:状態。グローバル変数のようなもの。単純(int,String等)、複雑(Listやクラス等)、Future(単純・複雑)、Stream(単純・複雑)等の種類がある。
Provider : 変数等の「状態(State)」をラップしているもの。状態の型に合わせて選択する必要があるが、後で説明するジェネレータを使うと意識しなくてもOK
Notifier:「状態(State)」を変更する機能。後で説明するジェネレータを使うと、_$xxxNotifier と1つの名前で統一して書ける。
main()関数では、MyApp()関数をProviderScope()関数で囲む必要がある。
Ver2でのデータの型と、対応するNotifier(状態変更ツール)、Provider(状態ラップ)
ジェネレータを使用しない場合は、手作業で適切なプロバイダを選択する必要がある。
単純系:intやString等の1つの変数
複雑系:リストやクラスのような複雑なもの
Future:後から値が確定する型
Stream:値がどんどん飛んでくるような型
データの型 | Notifier(状態変更ツール) | Provider(状態ラップ) |
単純データ・複雑データ | Notifier | NotifierProvider |
Future,Stream | AsyncNotifier | AsyncNotifierProvider |
riverpod_generator(build_runner) を使う場合は、_$xxxNotifierを1つ定義すると、後はジェネレータが適切な型のプロバイダを選択してソースをジェネレートしてくれる
パッケージのインストール
下記コマンドを、アプリのソースルートで実行する。(VSCodeのターミナルにコピペ)赤字は必須。(Hooksは別のページで説明)
flutter pub add flutter_riverpod
flutter pub add riverpod_annotation
flutter pub add dev:riverpod_generator
flutter pub add dev:build_runner
flutter pub add dev:custom_lint
flutter pub add dev:riverpod_lint
上記コマンドを実行すると、pubspec.yamlは、以下のようになる。(2025/2時点)
name: my_app_name
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: ">=3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.6.1
riverpod_annotation: ^2.6.1
dev_dependencies:
build_runner:
custom_lint:
riverpod_generator: ^2.6.4
riverpod_lint: ^2.6.4
Hello world
lib/main.dart 最小限のコード。赤字がRiverpodに関連している部分。この例では状態の変更(Notifier)は使っていない
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';
// 単純データ(String)の状態。「ref」が必要
@riverpod
String helloWorld(Ref ref) {
return 'Hello world';
}
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final String value = ref.watch(helloWorldProvider); //ConsumerWidgetがref.watchしている
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Center(
child: Text(value),
),
),
);
}
}
下記赤字のコマンドを実行して、ジェネレータを稼働させる。
(watchだと、ソース修正後にも自動でジェネレータが動くけど、ターミナルが使えなくなるから、下記を都度実行するのがおすすめです。)
flutter pub run build_runner build --delete-conflicting-outputs
ジェネレータ(build_runner)がつける、NotifierとProviderの名前のルール
Notifier: _$S1Notifier (先頭に_$をつけ、大文字で始まる)
Provider: s1NotifierProviderとなる。(先頭が小文字になり、最後にProviderが追加される)
文字列リスト状態をRiverpodで扱っているサンプル。(要ジェネレータ)
YouTuber ルビーDOGさんのRiverpod完全解説動画のソースを参考に、2025年2月時点の最新版のflutterで動くよう修正しています。
s1.dart 単純変数の例
//s1.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 's1.g.dart';
@riverpod
class S1Notifier extends _$S1Notifier {
@override
int build() {
// 最初のデータ
return 0;
}
// データを変更する関数
void updateState() {
// 変更前のデータ
final oldState = state;
// 変更後のデータ
final newState = oldState + 1;
// データを上書き
state = newState;
}
}
my_widge1.dart
// my_widget1.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 's1.dart';
class MyWidget1 extends ConsumerWidget {
const MyWidget1({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// S1 watch
final s1 = ref.watch(s1NotifierProvider);
// S1 listen
ref.listen(
s1NotifierProvider,
(oldState, newState) {
// スナックバーを表示
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('S1データが変更されました'),
),
);
},
);
// S1 テキスト
final s1Text = Text('$s1');
// S1 ボタン
final s1Button = ElevatedButton(
onPressed: () {
// S1 ノティファイアを呼ぶ
final notifier = ref.read(s1NotifierProvider.notifier);
// S1 データを変更
notifier.updateState();
},
child: const Text('+1'),
);
// 縦に並べる
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
s1Text,
s1Button,
],
);
}
}
s2.dart リスト・配列の例
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 's2.g.dart';
@riverpod
class S2Notifier extends _$S2Notifier {
@override
List<String> build() {
// 最初のデータ
return ['A', 'B', 'C', 'D'];
}
// データを変更する関数
void updateState() {
// 変更前のデータ
final oldState = state;
// 変更後のデータ
final newState = [...oldState, 'X'];
// データを上書き
state = newState;
}
}
my_widget2.dart
import 'package:flutter/material.dart';
import 's2.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MyWidget2 extends ConsumerWidget {
const MyWidget2({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) { // ref を作成
// S2 watchの例
final s2 = ref.watch(s2NotifierProvider); // buildの中ではref.watch
// ListView
final listView = ListView.builder(
itemCount: s2.length,
itemBuilder: (_, index) {
// index番目の文字
final text = Text(s2[index]);
return Card(child: text);
},
);
//S2 listenの例
ref.listen(s2NotifierProvider, (oldState, newState) {
// 関数を呼び出す。ここではスナックバーに値の変化を表示する。
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('$oldState → $newState')));
});
// ボタン
final button = FloatingActionButton(
onPressed: () {
// S2 Notifier
final notifier = ref.read(s2NotifierProvider.notifier); //onPressedの中ではref.read
// データを変更
notifier.updateState(); //notifierの関数を呼んで、状態を変化させる
},
child: const Icon(Icons.add),
);
// 縦に並べる
return Scaffold(floatingActionButton: button, body: listView);
}
}
main.dart
import 'package:flutter/material.dart';
import 'my_widget2.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
const app = MyApp();
const scope = ProviderScope(child: app);
runApp(scope);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Column(
mainAxisAliment: MainAxisAliment.center,
children:[
MyWidget1(),
MyWidget2(),
],
),
),
);
}
}
データ型毎のState表記例
// 単純データ
@riverpod
String example(ExampleRef ref) {
return 'foo';
}
// 複雑データ(List、クラス)
@riverpod
class Example extends _$Example {
@override
String build() {
return 'foo';
}
// Add methods to mutate the state
}
//Future 単純型
@riverpod
Future<String> example(ExampleRef ref) async {
return Future.value('foo');
}
//Stream 単純型
@riverpod
Stream<String> example(ExampleRef ref) async* {
yield 'foo';
}
//Future 複雑型
@riverpod
class Example extends _$Example {
@override
Future<String> build() async {
return Future.value('foo');
}
// Add methods to mutate the state
}
//Stream 複雑型
@riverpod
class Example extends _$Example {
@override
Stream<String> build() async* {
yield 'foo';
}
// Add methods to mutate the state
}
ref.watch, ref.read, ref.listenの違い
ref.watch は、値の変化に応じてウィジェットやプロバイダを更新する。(基本的にはwatichを使う)
ref.listen は、任意の関数を呼び出したい時に使用する。
ref.watchメソッドとref.listenメソッドは、 ElevatedButton の onPressed 内など、非同期的な場面で呼び出してはならない。
また initState を始め、State のライフサイクルメソッド内での使用も避けなければならない。
これらの場合は代わりに 下記のref.readメソッドを使用する。
ref.readメソッドは、単純に値を参照するのみ。ref.read はリアクティブではないため、可能な限り使用を避ける。
(watch や listen の使用では問題が生じる場合の回避策として存在している。)
ほとんどの場面では watch や listen の使用、特に watch の使用がベター。
AsyncValueについて
Future,Stream型では、データが存在しなかったり、エラーとなる場合が発生する。下記のように使用する。
具体的にはs3,s4のソースを参照。(省略。ルビーDOGさんのYouTubeでダウンロードできる)
final s3 = ref.watch(s3NotifierProvider);
// S3 AsyncValue
final s3Text = s3.when(
loading: () => const Text('準備中...'),
error: (e, s) => Text('エラー $e'), // e:エラーの種類、s:エラーが発生した場所
data: (d) => Text(d),
);
応用編
select
selectは、stateの一部のみwatchするような場合に使用する。以下はルビーDOGさんのサンプルを修正したものです。
shikoku.dart
// freezed 使えばもっと短く書ける
// 四国
class Shikoku {
const Shikoku({
required this.kagawa,
required this.tokushima,
required this.kochi,
required this.ehime,
});
// 香川
final int kagawa;
// 徳島
final int tokushima;
// 高知
final int kochi;
// 愛媛
final int ehime;
/// copyWith
Shikoku copyWith({
int? kagawa,
int? tokushima,
int? kochi,
int? ehime,
}) {
return Shikoku(
kagawa: kagawa ?? this.kagawa,
tokushima: tokushima ?? this.tokushima,
kochi: kochi ?? this.kochi,
ehime: ehime ?? this.ehime,
);
}
/// ==
@override
bool operator ==(Object other) {
return other is Shikoku &&
other.runtimeType == runtimeType &&
other.kagawa == kagawa &&
other.tokushima == tokushima &&
other.kochi == kochi &&
other.ehime == ehime;
}
/// hashCode
@override
int get hashCode {
return Object.hash(
runtimeType,
kagawa,
tokushima,
kochi,
ehime,
);
}
}
state.dart (ジェネレータが必要)
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shikoku.dart';
part 'state.g.dart';
@riverpod
class ShikokuNotifier extends _$ShikokuNotifier {
@override
Shikoku build() {
// 人口
return const Shikoku(
kagawa: 93,
tokushima: 70,
kochi: 69,
ehime: 130,
);
}
void updateKagawa() {
final oldState = state;
final newState = oldState.copyWith(
kagawa: oldState.kagawa + 1,
);
state = newState;
}
void updateTokushima() {
final oldState = state;
final newState = oldState.copyWith(
tokushima: oldState.tokushima + 1,
);
state = newState;
}
void updateKochi() {
final oldState = state;
final newState = oldState.copyWith(
kochi: oldState.kochi + 1,
);
state = newState;
}
void updateEhime() {
final oldState = state;
final newState = oldState.copyWith(
ehime: oldState.ehime + 1,
);
state = newState;
}
}
widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'state.dart';
class MyWidget extends ConsumerWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 四国の状態を watch
final shikoku = ref.watch(shikokuNotifierProvider);
// 香川だけを listen (select)
ref.listen(
shikokuNotifierProvider.select(
(shikoku) => shikoku.kagawa,
),
(oldState, newState) {
// スナックバーを表示する
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$oldState から $newState へ変化しました'),
),
);
},
);
// 人口たち
final popurations = Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('${shikoku.kagawa}'),
Text('${shikoku.tokushima}'),
Text('${shikoku.kochi}'),
Text('${shikoku.ehime}'),
],
);
// ボタンたち
final buttons = Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
final notifier = ref.read(shikokuNotifierProvider.notifier);
notifier.updateKagawa();
},
child: const Text('香川'),
),
ElevatedButton(
onPressed: () {
final notifier = ref.read(shikokuNotifierProvider.notifier);
notifier.updateTokushima();
},
child: const Text('徳島'),
),
ElevatedButton(
onPressed: () {
final notifier = ref.read(shikokuNotifierProvider.notifier);
notifier.updateKochi();
},
child: const Text('高知'),
),
ElevatedButton(
onPressed: () {
final notifier = ref.read(shikokuNotifierProvider.notifier);
notifier.updateEhime();
},
child: const Text('愛媛'),
),
],
);
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 人口たち
popurations,
// ボタンたち
buttons,
],
);
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'widget.dart';
void main() {
const app = MayApp();
const scope = ProviderScope(child: app);
runApp(scope);
}
class MayApp extends StatelessWidget {
const MayApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
buildとonDispose
最初のwatch時にbuildが動き、最初の状態が確定する。誰も状態をwatchしなくなったら、onDisposeが動く。(buildの中に記載する)
Riverpod2 では、基本的にautoDisposeとなっており、データを破棄されたくなければ、次のkeepAliveを指定する必要がある。
(ソースは省略)
keepAlive
データを捨てられなくする。@Riverpod ( keepAlive : true ) 最初が大文字のRなのがポイント。
(Riverpod v1ではkeepAliveがデフォルトだったが、v2からは onDisposeがデフォルト)
state.dart (ジェネレータが必要)
import 'package:flutter/widgets.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'state.g.dart';
//@riverpod //v2ではonDisposeがデフォルト。
@Riverpod(keepAlive: true) // v1と同じ動き。onDisposeが働かなくなる
class CountNotifier extends _$CountNotifier {
@override
int build() {
// 最初のデータを準備する
debugPrint('誰かにwatchされたのでデータを準備します');
// データが捨てられた時のことを決めておく
ref.onDispose(() {
debugPrint('誰にもwatchされなくなったのでデータを捨てます');
});
return 0;
}
// データを変更する関数
void updateState() {
final oldState = state;
final newState = oldState + 1;
state = newState;
}
}
main.dart (ルビーDOGさんのソースをかなり修正しています。赤字部分が修正箇所)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'state.dart';
void main() {
const app = MyApp();
const scope = ProviderScope(child: app);
runApp(scope);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: '/1',
routes: {
'/1': (context) => const Page1(),
'/2': (context) => const Page2(),
'/3': (context) => const Page3(),
},
);
}
}
class Page1 extends StatelessWidget {
const Page1({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
title: const Text('1'),
actions: [
IconButton(
icon: const Icon(Icons.arrow_forward_ios),
onPressed: () {
Navigator.of(context).pushNamed('/2');
},
),
],
),
body: const Center(),
);
}
}
class Page2 extends ConsumerWidget {
const Page2({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final int count = ref.watch(countNotifierProvider);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.green,
title: const Text('2'),
actions: [
IconButton(
icon: const Icon(Icons.arrow_forward_ios),
onPressed: () {
Navigator.of(context).pushNamed('/3');
},
),
],
),
body: Center(child: Text('${count}')),
);
}
}
class Page3 extends ConsumerWidget {
const Page3({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final int count = ref.watch(countNotifierProvider);
return Scaffold(
appBar: AppBar(backgroundColor: Colors.blue, title: const Text('3')),
body: Center(child: Text('${count}')),
floatingActionButton: FloatingActionButton(
onPressed: () {
final notifier = ref.read(countNotifierProvider.notifier);
notifier.updateState();
},
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Basic Provider
Basic Provider (単にProviderと表記) Notifierが存在しないProvider。
refを使って、Notifierをref.readして他のNotifireProviderのNotifierを呼び出せる。
ConsumerWegetの原型でもある。
-proxy
複数のStateを監視して1つのデータに纏める時に便利。例えば、s1,s2,s3の3つのNotifierProviderが持つstateを同時に監視して、s1+s2+s3を返すようなことができる。
-logic
NotifierProviderのデータを監視して、ある条件が満たされた場合に、stateが変化する。重たい処理をするwatchが、stateが変化するたびに、無駄に動作しないようにできるメリットがある。
-cache
データをキープして再計算を避けるために準備されたプロバイダ。
ProviderFamily
似たようなプロバイダを複数準備するのではなく、IDを与えることで、1つの記述で内部動作を変え、あたかも別のプロバイダがあるように見せる仕組み。
family.dart (ジェネレータが必要)
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'family.g.dart';
@riverpod
int family(FamilyRef ref, String id) {
if (id == '日本') {
return 3;
}
if (id == 'アメリカ') {
return 2;
}
return 0;
}
widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'family.dart';
class MyWidget extends ConsumerWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final japan = ref.watch(familyProvider('日本'));
final usa = ref.watch(familyProvider('アメリカ'));
// WBC優勝おめでとう!
final wbcScore = Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('$japan'),
Text('$usa'),
],
);
return wbcScore;
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'widget.dart';
void main() {
const app = MayApp();
const scope = ProviderScope(child: app);
runApp(scope);
}
class MayApp extends StatelessWidget {
const MayApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
ProviderScope
デバッグ等で、複数のProviderScopeを活用することができる。普通はMyAppをProvideScopeで囲うが、実はPage単位でも囲える。別のスコープになる。以下のサンプルソースではflutter_hooksが必要。ターミナルで下記コマンドを実行すると、pubspec.yamlに追記される
flutter pub add flutter_hooks
state.dart (ジェネレータが必要)
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'state.g.dart';
@riverpod
class TestNotifier extends _$TestNotifier {
@override
int build() {
return 50;
}
void plus() {
final oldState = state;
final newState = oldState + 1;
state = newState;
}
void minus() {
final oldState = state;
final newState = oldState - 1;
state = newState;
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'state.dart';
main() {
const app = MyApp(
pages: [
// プロバイダースコープ
ProviderScope(child: PageA()),
// プロバイダースコープ
ProviderScope(child: PageB()),
],
);
runApp(app);
}
// アプリ
class MyApp extends HookWidget {
const MyApp({
super.key,
required this.pages,
});
final List<Widget> pages;
@override
Widget build(BuildContext context) {
final index = useState(0);
// タブのアイテムたち
const items = [
BottomNavigationBarItem(
icon: Icon(Icons.circle),
label: 'A',
),
BottomNavigationBarItem(
icon: Icon(Icons.circle),
label: 'B',
),
];
// タブバー
final bar = BottomNavigationBar(
items: items,
backgroundColor: Colors.orange,
selectedItemColor: Colors.black,
unselectedItemColor: Colors.white,
currentIndex: index.value,
onTap: (i) => index.value = i,
);
return MaterialApp(
home: Scaffold(
// プロバイダースコープがWidgetTreeから消えないようにするIndexedStack
body: IndexedStack(
index: index.value,
children: pages,
),
bottomNavigationBar: bar,
),
);
}
}
// 画面 A
class PageA extends ConsumerWidget {
const PageA({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// watch
final test = ref.watch(testNotifierProvider);
// ボタン
final button = FloatingActionButton(
backgroundColor: Colors.orange,
onPressed: () {
ref.read(testNotifierProvider.notifier).minus();
},
child: const Icon(Icons.remove),
);
// 画面
return Scaffold(
floatingActionButton: button,
body: Center(
child: Text('$test'),
),
);
}
}
// 画面 B
class PageB extends ConsumerWidget {
const PageB({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// watch
final test = ref.watch(testNotifierProvider);
// ボタン
final button = FloatingActionButton(
backgroundColor: Colors.orange,
onPressed: () {
ref.read(testNotifierProvider.notifier).plus();
},
child: const Icon(Icons.add),
);
// 画面
return Scaffold(
floatingActionButton: button,
body: Center(
child: Text('$test'),
),
);
}
}
overrideWith
NotifierProviderをフェイクのプロバイダに差し替えて、デバッグ等で使える。ProvideScope内で定義する。
final scope = ProvideScope(
overrides: [
originalProvider.overrideWith((ref){
return 'テストデータ';
}),
],
);
サンプルソース(動作確認済)
state.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'state.g.dart';
// 本物
@riverpod
String apple(AppleRef ref) {
return 'りんご';
}
widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'state.dart';
class MyWidget extends ConsumerWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// りんご ?
final apple = ref.watch(appleProvider);
return Text(apple);
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'state.dart';
import 'widget.dart';
void main() {
const app = MyApp();
final scope = ProviderScope(
overrides: [
// ここに偽物のデータを使いたいプロバイダーたち
appleProvider.overrideWith((ref) {
return '毒りんご';
}),
],
child: app,
);
runApp(scope);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}