Flutter 3.xが安定版になってきたので、全面的に書き直し。今回はgo_routerとの共存も含めたソースにしてます。
Flutter Riverpodの要点
用語説明と概要
一般的にはstate.dartというファイルに状態を維持するための変数を記載し、それを他のページでインポートして使う感じ。
ver.3ではジェネレータを使って自動で適切なProvider,Notifierを使うようにすることが前提(後述)
ConsumerWidget, ConsumerStatefulWidget: 外部の「状態(State)」を観察することが出来るWidget。主に内部変数を持たないシンプルなものと、内部変数を持つもの(Stateful)を使い分ける(他にも色々ある)
ref経由でProviderの中にあるStateを覗くことができる。(詳細は後述)
State:状態。グローバル変数のようなもの。単純(int,String等)、複雑(Listやクラス等)、Future(単純・複雑)、Stream(単純・複雑)等の種類がある。
Provider : 変数等の「状態(State)」をラップしているもの。
Notifier:「状態(State)」を変更するときに必須の機能。
main()関数では、MyApp()関数をProviderScope()関数で囲む必要がある。
特に注意すべき点
- レガシー表記が原則としてエラーとなります。(一応逃げ道はあるけど)
- ref.watch() またはref.listen()していないと、autoDispose()自動破棄がデフォルト。(ref.Readは、常に必要とされているとはみなされない)
「@riverpod」と記載するところを「@Riverpod(keepAlive: true)」とすると破棄されない。
(僕はこれにハマりました。ref.read()ばかり使ってたので。) ref.watch(): 画面を「書き直す(リビルド)」ために使う。(常に最新の状態を表示し続けたいとき)ref.listen(): 何か「アクションを起こす(副作用)」ために使う。(値が変わった瞬間だけ、1回処理を走らせたいとき)- go_routerで、context.go(/) とすると、初期化されてしまうことがある。
go_routerで文字列を渡す場合、push,popなら大丈夫。context.go('/')を使いたいときは、extra で値を渡すのが安全。
Flutter 3.x のサンプルソース
いきなりですが、完全動作するサンプルソースを提示します。2026年2月時点のパッケージバージョンで、動作確認済みです。
このサンプルの仕様
ホームページ、画面A、画面Bがあります。
ホームページのドロワーメニューから画面Aを起動し、例えば画面Aで撮影し、画面Bで画像処理するように、画面AB間を行き来できるようにする。(今回は、画面Aは完全なダミーで、画面Bで文字列を入力するようになってます。)
画面Bでデータが確定したら、ホーム画面でデータを取得し表示します。画面ABは閉じられます。
モジュール追加のコツ
モジュールの依存関係は、とても複雑です。特にRiverpod絡みは日々変わります。SDKもガンガンバージョンアップします。
したがって、モジュールを1つ1つ登録するのではなく、一気に登録した方が、最新のモジュールで、かつ依存関係も完璧になります。
flutter pub add flutter_riverpod riverpod_annotation go_router build_runner riverpod_generator
ただ、このままだと全てアプリに含まれてしまうので、build_runnerとriverpod_generatorは、手作業で「dev_dependencies」の下に移しましょう!(ここがかなり重要です!)
サンプル全ソース(String型を共有するだけのシンプルな構成)
重要な部分を赤色にしています。なお、モジュールのバージョンは、2026年2月の依存関係が成り立つ最新版です。
最新版のモジュールを使いたい場合は、上記方法でモジュール追加してください。
pubspec.yaml
name: test_riverpod3
description: "A new Flutter project."
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: '>=3.10.8 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^3.1.0
riverpod_annotation: ^4.0.0
go_router: ^17.1.0
# iOS スタイルのアイコンには CupertinoIcons クラスで使用します。
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.11.0
riverpod_generator: ^4.0.0+1
# 以下の「flutter_lints」パッケージには、優れたコーディングプラクティスを促進するための推奨lintセットが含まれています。
#lint とは、コードを実行せずに解析して、バグになりやすい書き方 や スタイルの問題 を警告する仕組みのことです。
# このパッケージが提供するlintセットは、パッケージのルートにある `analysis_options.yaml` ファイルで有効化されます。
# 特定のlintルールを無効化したり、追加のlintルールを有効化したりする方法については、このファイルを参照してください。
flutter_lints: ^6.0.0
flutter:
# 次の行は、Material Icons フォントがアプリケーションに含まれ、Material Icons クラスのアイコンが使用できるようにします。
uses-material-design: true
# assetsの記載例です。
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# アプリケーションにカスタムフォントを追加するには、
# この「flutter」セクションにフォントセクションを追加してください。
# このリストの各エントリには、フォントファミリー名を含む「family」キーと、
# フォントのアセットやその他の記述子を含むリストを含む「fonts」キーが必要です。
# 例:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
main.dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
// 各ページファイルをインポート
import 'home_page.dart';
import 'page_a.dart';
import 'page_b.dart';
void main() {
// ProviderScopeでアプリ全体をラップする
runApp(const ProviderScope(child: MyApp()));
}
// GoRouterの定義
final _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/a',
builder: (context, state) => const PageA(),
),
GoRoute(
path: '/b',
builder: (context, state) => const PageB(),
),
],
);
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
title: 'Riverpod Split File Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
);
}
}
shared_state.dart
// lib/shared_state.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
// ファイル名と同じ名前で .g.dart を指定します(要build_runner)
part 'shared_state.g.dart';
@riverpod
class SharedString extends _$SharedString {
@override
String? build() {
// 初期値は null (未設定)
return null;
}
// 値を更新するメソッド
void update(String newValue) {
state = newValue;
}
}
home_page.dart (この例では内部変数のないシンプルなConsumerWidgetになってます。)
// lib/home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'shared_state.dart'; // 状態ファイルをインポート
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 状態を監視。変更があればこのWidgetだけ再描画される
final sharedValue = ref.watch(sharedStringProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
// ドロワーメニュー
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Colors.indigo),
child: Text(
'Menu',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
ListTile(
leading: const Icon(Icons.arrow_forward),
title: const Text('ページAへ移動'),
onTap: () {
// ドロワーを閉じてから移動
Navigator.pop(context);
context.push('/a');
},
),
],
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'ページBから受け取ったデータ:',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 10),
Text(
sharedValue ?? '(データなし)',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.indigo
),
),
],
),
),
);
}
}
ConsumerStatefullWidgetでは、以下の点に注意してください。
・buildに「WidgetRef ref」の記載が不要
・StateオブジェクトはConsumerStateオブジェクトになる。
page_a.dart
// lib/page_a.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class PageA extends StatelessWidget {
const PageA({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page A')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.looks_one, size: 80, color: Colors.grey),
const SizedBox(height: 20),
const Text('ここはページAです'),
const SizedBox(height: 20),
ElevatedButton.icon(
onPressed: () {
// スタックの上にページBを積む
context.push('/b');
},
icon: const Icon(Icons.arrow_forward),
label: const Text('ページBへ進む'),
),
],
),
),
);
}
}
page_b.dart
// lib/page_b.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'shared_state.dart'; // 状態ファイルをインポート
class PageB extends ConsumerStatefulWidget {
const PageB({super.key});
@override
ConsumerState<PageB> createState() => _PageBState();
}
class _PageBState extends ConsumerState<PageB> {
// 入力コントローラー
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose(); // メモリリーク防止のため破棄
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page B')),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Homeへ送るデータを入力してください',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 20),
TextField(
controller: _controller,
decoration: const InputDecoration(
labelText: 'メッセージ',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.edit),
),
),
const SizedBox(height: 30),
FilledButton(
onPressed: () {
final text = _controller.text;
if (text.isEmpty) return;
// 1. Riverpodの状態を更新 (notifierをreadしてメソッドを呼ぶ)
ref.read(sharedStringProvider.notifier).update(text);
// 2. Homeに戻る
// context.go('/') は、これまでの履歴(A, B)を破棄して
// ルート('/')を新規に表示します。
context.go('/');
},
child: const Padding(
padding: EdgeInsets.all(12.0),
child: Text('保存してHomeに戻る', style: TextStyle(fontSize: 16)),
),
),
],
),
),
);
}
}
ビルド用のbashスクリプト
flutter には様々なビルド手順が必要なので、次のようなビルド用スクリプトを準備したほうがよい。(Ubuntuの場合)
run_build.sh
#!/bin/bash
# ==========================================
# Flutter Build Assistant
# ==========================================
# 色の定義
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# ------------------------------------------
# 関数定義
# ------------------------------------------
# 通常ビルドの処理
run_normal_build() {
echo -e "${YELLOW}[Normal] Cleaning project...${NC}"
flutter clean
echo -e "${YELLOW}[Normal] Getting packages...${NC}"
flutter pub get
# build_runner が pubspec.yaml にあるか確認
if grep -q "build_runner:" pubspec.yaml; then
echo -e "${GREEN}[Normal] build_runner detected. Running build...${NC}"
dart run build_runner build --delete-conflicting-outputs
else
echo -e "${BLUE}[Normal] build_runner not found. Skipping.${NC}"
fi
# flutter_launcher_icons が pubspec.yaml にあるか確認
if grep -q "flutter_launcher_icons:" pubspec.yaml; then
echo -e "${GREEN}[Normal] flutter_launcher_icons detected. Generating icons...${NC}"
dart run flutter_launcher_icons
else
echo -e "${BLUE}[Normal] flutter_launcher_icons not found. Skipping.${NC}"
fi
}
# 拡張ビルドの処理
run_extended_build() {
echo -e "${RED}[Extended] Upgrading Flutter SDK...${NC}"
flutter upgrade
echo -e "${RED}[Extended] Clearing global package cache...${NC}"
flutter pub cache clean -f
echo -e "${RED}[Extended] Upgrading project packages...${NC}"
flutter pub upgrade
# 最後に通常ビルドを実行
run_normal_build
}
# ------------------------------------------
# メインメニュー
# ------------------------------------------
echo -e "${BLUE}=== Flutter Build Menu ===${NC}"
echo "1. 通常ビルド (Clean, Get, Build, Icons)"
echo "2. 拡張ビルド (Upgrade SDK/Modules, Cache Clear + Normal)"
echo "3. Quit"
echo -e "${BLUE}==========================${NC}"
read -p "番号を入力してください (1-3): " option
case $option in
1)
echo -e "${GREEN}>>> Starting Normal Build <<<${NC}"
run_normal_build
echo -e "${GREEN}>>> Normal Build Completed <<<${NC}"
;;
2)
echo -e "${RED}>>> Starting Extended Build <<<${NC}"
echo "※ SDKのアップグレードやキャッシュクリアには時間がかかります"
run_extended_build
echo -e "${GREEN}>>> Extended Build Completed <<<${NC}"
;;
3)
echo "終了します。"
exit 0
;;
*)
echo -e "${RED}無効な選択です。終了します。${NC}"
exit 1
;;
esac
Riverpod3.xで、シンプルな配列を扱う
Flutter Riverpod 3.1.0(および riverpod_generator)環境において、配列(List)を扱う際の鉄則と具体的な実装パターンを解説します。
バージョン3系でも、最も重要なルールは 「不変性(Immutability)」 です。
Riverpodは state の中身(メモリ上の参照先)が新しいものに置き換わらない限り、変更を検知しません。
つまり、state.add(item) は禁止です。必ず state = [...state, item] のように新しい配列を作って代入します。
基本的な実装例(文字列のリスト)
まずはシンプルな List<String> を扱う例です。
string_list.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'string_list.g.dart'; // ファイル名.g.dartになる
@riverpod
class StringList extends _$StringList {
@override
List<String> build() {
return []; // 初期値(空の配列)
}
// 【追加】スプレッド演算子(...)を使って新しい配列を作成
void add(String item) {
state = [...state, item];
}
// 【削除】条件に合う要素を除外した新しい配列を作成
void remove(String target) {
state = [
for (final item in state)
if (item != target) item,
];
// または: state = state.where((i) => i != target).toList();
}
// 【更新】特定のインデックスを書き換える
void updateAt(int index, String newItem) {
if (index < 0 || index >= state.length) return;
// 配列のコピーを作成
final newState = [...state];
// コピーを書き換え
newState[index] = newItem;
// 新しい配列をセット
state = newState;
}
}
Riverpod3.xで、クラスを扱う
Riverpod 3系でクラスを扱う場合、不変性(Immutability)と copyWith(値の更新用メソッド)が必須となるため、freezed パッケージとの併用がデファクトスタンダード(事実上の標準)です。
下記モジュールが必要です。なお、モジュールの追加は、flutter pub addで一気に追加し、次に開発時のみ必要なものをdev_dependencies以下に移しましょう。
pubspec.yaml (2026年2月時点での例)
environment:
# Riverpod 3系は新しいDartの機能を使うため、最新のSDK指定を推奨
sdk: '>=3.5.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# --- Riverpod 3.x 本体 ---
flutter_riverpod: ^3.1.0
# コード生成用のアノテーション (@riverpod)
riverpod_annotation: ^3.1.0
# --- クラス生成用 (Freezed) ---
# データクラスを不変にし、copyWithや==演算子を自動生成する
freezed_annotation: ^2.4.4
# --- JSON対応 (API通信をするなら必要) ---
json_annotation: ^4.9.0
dev_dependencies:
flutter_test:
sdk: flutter
# --- ビルドランナー (コード生成の実行役) ---
build_runner: ^2.4.13
# --- Riverpodジェネレーター ---
# Riverpodのアノテーションを見てコードを生成
riverpod_generator: ^3.1.0
# --- Freezedジェネレーター ---
# Freezedのアノテーションを見てデータクラスを生成
freezed: ^2.5.7
# --- JSONジェネレーター ---
json_serializable: ^6.9.0
実装サンプル
Step 1: データクラスの定義 (user.dart)
@freezed を使うことで、面倒な copyWith メソッドを手動で書く必要がなくなります。
user.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart'; // json_serializableを使う場合
@freezed
class User with _$User {
// 不変クラスとして定義
const factory User({
required String id,
required String name,
@Default(0) int age,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
Step 2: Riverpodでの利用 (user_notifier.dart)
Riverpod 3 の Generator 構文と組み合わせることで、非常に簡潔に記述できます。
user_notifier.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'user.dart';
part 'user_notifier.g.dart';
@riverpod
class UserNotifier extends _$UserNotifier {
@override
User build() {
return const User(id: '1', name: 'Guest');
}
void updateName(String newName) {
// Freezedのおかげで .copyWith が使える
// これにより「不変性を保ったままの一部更新」が可能になる
state = state.copyWith(name: newName);
}
}
run_build.shの実行を忘れずに!