Flutter: 状態管理 — Riverpod
状態はアプリの命の源です — 管理を誤ると「内出血」を引き起こします:データの不整合、リビルドストーム、メモリリーク。
📋 前提条件: 以下を先にマスターしている必要があります
- レッスン5:StatefulWidgetとインタラクション
- レッスン11:ネットワークとREST API
1. このレッスンで学ぶこと
- プロバイダーシステム:Provider / StateProvider / FutureProvider / StreamProvider / NotifierProvider
- Riverpod 2.x:Notifier / AsyncNotifier / @riverpodアノテーションコード生成
- ref.watch / ref.read / ref.listenの使用シナリオとパフォーマンスのトレードオフ
- ProviderScopeとConsumerWidget / ConsumerStatefulWidget
- ShopApp:CartNotifierによるショッピングカート + AsyncProductNotifierによる非同期商品読み込み
2. 状態災害のリアルなストーリー
(1) 悩み:setStateによるグローバル状態のジレンマ
BobのShopAppはsetStateでショッピングカートの状態を管理しています。問題点:カートデータはホーム、詳細、カートの各ページで必要ですが、ウィジェットツリーのすべての層をコールバックで渡す必要があります。CartPageで数量を変更しても、HomePageのAppBarバッジは更新されません — なぜならHomePageのStateはCartPageの変更を知らないからです。さらに悪いことに、10のページが5つのカートデータのコピーを持ち、すべて同期されていません。
(2) Riverpodのソリューション
Riverpodは状態をグローバルプロバイダーに引き上げます。任意のページが同じカート状態をref.watchでき、ある場所での変更がすべてのリスナーを自動的に更新します。
DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod riverpod_annotation
// ⚙️ Dev dependency: flutter pub add --dev riverpod_generator build_runner
// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - CartItem: see CartNotifier example below
// Global cart state - any page can access
@riverpod
class CartNotifier extends _$CartNotifier {
@override
List<CartItem> build() => [];
void addItem(Product product) {
state = [...state, CartItem(product: product, quantity: 1)];
}
void removeItem(int productId) {
state = state.where((item) => item.product.id != productId).toList();
}
}
// Watch in any widget
final cart = ref.watch(cartNotifierProvider);
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(3) メリット:1つの変更がグローバルに同期
Riverpodを使うと、Bobのカート状態は1箇所に存在し — 任意のページでの変更がすべてのリスナーを自動的に更新します。コールバック地獄は消え、データは一貫性を保ちます。
3. Riverpodのコア概念
graph TD
PS[ProviderScope] --> CN[CartNotifier]
PS --> PN[ProductAsyncNotifier]
PS --> UN[UserNotifier]
CN --> |ref.watch| CV[CartView]
PN --> |ref.watch| PV[ProductListView]
UN --> |ref.watch| UV[UserProfileView]
CN --> |totalItems| Badge[BottomNav Badge]
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(1) プロバイダータイプ
| プロバイダー | 状態タイプ | ユースケース |
|---|---|---|
Provider |
不変の値 | 依存性注入、設定 |
StateProvider |
シンプルな可変値 | カウンター、トグル |
FutureProvider |
Futureの結果 | 一回限りの非同期データ |
StreamProvider |
Streamの結果 | リアルタイムデータストリーム |
NotifierProvider |
Notifierインスタンス | 複雑な状態 + ビジネスロジック |
AsyncNotifierProvider |
AsyncNotifier | 非同期初期化 + ビジネスロジック |
(2) refメソッド比較
| メソッド | 目的 | リビルドをトリガー | 使用場所 |
|---|---|---|---|
ref.watch |
状態変化を監視 | はい(状態変化時) | buildメソッド内 |
ref.read |
一回限りの読み取り | いいえ | コールバック / イベントハンドラ |
ref.listen |
監視 + 副作用の実行 | いいえ | build内(ナビゲーション/SnackBar) |
⚠️ 注意: buildの外でref.watchを使わず、build内で状態を監視するためにref.readを使わないでください。
4. 手書きプロバイダー
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:StateProviderによるシンプルな状態
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod
// Simple counter with StateProvider
final counterProvider = StateProvider<int>((ref) => 0);
// Theme mode provider
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
// Usage in widget
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Text('Increment'),
),
],
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:FutureProviderによる非同期データ
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod
// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - ProductTile: custom Widget for displaying a single product
// - productRepositoryProvider: see Lesson 14 ProductRepository
// Load products once
final productsProvider = FutureProvider<List<Product>>((ref) async {
final repo = ref.read(productRepositoryProvider);
return repo.getProducts();
});
// Usage
class ProductList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productsProvider);
return productsAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, _) => Text('Error: $err'),
data: (products) => ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) => ProductTile(product: products[i]),
),
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
5. NotifierとAsyncNotifier
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:CartNotifier(完全なショッピングカートロジック)
DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod
// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model (simplified version below)
/*
class Product {
final int id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
*/
class CartItem {
final Product product;
final int quantity;
const CartItem({required this.product, required this.quantity});
double get total => product.price * quantity;
CartItem copyWith({int? quantity}) => CartItem(product: product, quantity: quantity ?? this.quantity);
}
class CartNotifier extends Notifier<List<CartItem>> {
@override
List<CartItem> build() => [];
void addItem(Product product) {
final idx = state.indexWhere((i) => i.product.id == product.id);
if (idx >= 0) {
state = [...state]..[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
} else {
state = [...state, CartItem(product: product, quantity: 1)];
}
}
void updateQuantity(int productId, int quantity) {
if (quantity <= 0) {
removeItem(productId);
return;
}
state = [
for (final item in state)
if (item.product.id == productId) item.copyWith(quantity: quantity) else item,
];
}
void removeItem(int productId) {
state = state.where((i) => i.product.id != productId).toList();
}
void clear() => state = [];
}
final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>(CartNotifier.new);
// Derived state: total price
final cartTotalProvider = Provider<double>((ref) {
final items = ref.watch(cartProvider);
return items.fold(0.0, (sum, item) => sum + item.total);
});
// Derived state: item count
final cartItemCountProvider = Provider<int>((ref) {
final items = ref.watch(cartProvider);
return items.fold(0, (sum, item) => sum + item.quantity);
});
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:AsyncNotifier(非同期商品初期化)
DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod
// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - productRepositoryProvider: see Lesson 14 ProductRepository
class ProductAsyncNotifier extends AsyncNotifier<List<Product>> {
@override
Future<List<Product>> build() async {
final repo = ref.read(productRepositoryProvider);
return repo.getProducts();
}
Future<void> loadMore() async {
final current = state.valueOrNull ?? [];
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final more = await ref.read(productRepositoryProvider).getProducts(page: (current.length ~/ 20) + 1);
return [...current, ...more];
});
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => ref.read(productRepositoryProvider).getProducts());
}
}
final productProvider = AsyncNotifierProvider<ProductAsyncNotifier, List<Product>>(
ProductAsyncNotifier.new,
);
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
6. @riverpodコード生成
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:riverpod_generatorの使用
DART
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod riverpod_annotation
// ⚙️ Dev dependency: flutter pub add --dev riverpod_generator build_runner
// Custom class definition sources:
// - CartItem: see Lesson 5 CartNotifier example in this lesson
// - User/AuthService: see Lesson 14 Auth Notifier
part 'providers.g.dart';
// Generated notifier
@riverpod
class Cart extends _$Cart {
@override
List<CartItem> build() => [];
void addItem(Product product) {
final idx = state.indexWhere((i) => i.product.id == product.id);
if (idx >= 0) {
state[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
state = [...state]; // Trigger rebuild
} else {
state = [...state, CartItem(product: product, quantity: 1)];
}
}
}
// Generated provider (derived)
@riverpod
double cartTotal(CartTotalRef ref) {
final items = ref.watch(cartProvider);
return items.fold(0.0, (sum, item) => sum + item.total);
}
// Keep alive across widget lifecycle
@Riverpod(keepAlive: true)
class Auth extends _$Auth {
@override
AsyncValue<User?> build() => const AsyncData(null);
Future<void> login(String email, String password) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => AuthService.login(email, password));
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
7. 完全な例:ShopApp Riverpod統合
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod go_router
// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - cartProvider/cartTotalProvider/cartItemCountProvider: see Lesson 5 in this lesson
// - themeModeProvider: see Lesson 4 StateProvider in this lesson
// main.dart
void main() {
runApp(ProviderScope(child: ShopApp()));
}
class ShopApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
return MaterialApp.router(
title: 'ShopApp',
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
darkTheme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true, brightness: Brightness.dark),
themeMode: themeMode,
routerConfig: router,
);
}
}
// Cart badge in AppBar - auto-updates
class CartBadge extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(cartItemCountProvider);
return Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shopping_cart),
if (count > 0)
Positioned(right: 0, top: 0,
child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
child: Text('$count', style: const TextStyle(fontSize: 9, color: Colors.white)))),
],
);
}
}
// Add to cart action
class AddToCartButton extends ConsumerWidget {
final Product product;
const AddToCartButton({super.key, required this.product});
@override
Widget build(BuildContext context, WidgetRef ref) {
return FilledButton.icon(
onPressed: () {
ref.read(cartProvider.notifier).addItem(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} added to cart')),
);
},
icon: const Icon(Icons.shopping_cart),
label: const Text('Add to Cart'),
);
}
}
// Cart page with Riverpod
class CartPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(cartProvider);
final total = ref.watch(cartTotalProvider);
return Scaffold(
appBar: AppBar(title: Text('Cart (${items.length})')),
body: items.isEmpty
? const Center(child: Text('Cart is empty'))
: Column(children: [
Expanded(child: ListView.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (_, i) {
final item = items[i];
return ListTile(
title: Text(item.product.name),
subtitle: Text('\$${item.total.toStringAsFixed(2)}'),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(icon: const Icon(Icons.remove), onPressed: () =>
ref.read(cartProvider.notifier).updateQuantity(item.product.id, item.quantity - 1)),
Text('${item.quantity}'),
IconButton(icon: const Icon(Icons.add), onPressed: () =>
ref.read(cartProvider.notifier).updateQuantity(item.product.id, item.quantity + 1)),
]),
);
},
)),
Padding(padding: const EdgeInsets.all(16),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text('\$${total.toStringAsFixed(2)}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
FilledButton(onPressed: () => context.push('/checkout'), child: const Text('Checkout')),
])),
]),
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
❓ よくある質問
Q RiverpodとProviderパッケージの違いは何ですか?
A RiverpodはProviderの進化版であり、Providerの安全でない依存性注入とテストの問題を解決しています。新しいプロジェクトではRiverpodを使用してください。
Q ref.watchとref.readのどちらを選ぶべきですか?
A build内でUIが状態変化に応答する必要がある場合はref.watchを使い、コールバック/イベントハンドラではref.readを使います(リビルド不要)。
Q state = [...state]は必要ですか?
A はい。Riverpodはidenticalチェックで状態変化を検出します。Listの内容を変更しても新しいオブジェクトは作成されないため、Riverpodは更新を通知しません。
Q AsyncNotifierでAsyncLoading/AsyncData/AsyncErrorをどう処理しますか?
A
.when(loading:, error:, data:)パターンマッチングを使って、ローディングインジケーター、エラーページ、データコンテンツをそれぞれ表示します。Q @riverpodアノテーションにはどの依存関係が必要ですか?
A pubspec.yamlにriverpod_annotation + riverpod_generator + build_runnerを追加し、
dart run build_runner watchを実行します。Q keepAliveとautoDisposeの違いは何ですか?
A autoDispose(デフォルト)は誰も監視していないときにプロバイダーを破棄します。keepAliveは決して破棄しません。グローバル状態(Authなど)にはkeepAliveを使用します。
📖 まとめ
- Riverpodはグローバル状態を統一的に管理 — 1つの変更がすべてのリスナーを自動更新
- Notifierは複雑な状態 + ビジネスロジックに適し、AsyncNotifierは非同期初期化に適しています
- ref.watchはbuild内で監視、ref.readはコールバックで操作、ref.listenは副作用を処理
- 派生プロバイダー(cartTotalProvider)は依存状態を自動計算
- @riverpodコード生成はボイラープレートを削減 — 新規プロジェクトに推奨
📝 練習問題
- 基本 (⭐):StateProviderを使ってダークモード切り替えを実装し、ConsumerWidgetでAppBarに現在のモードを表示してください。
- 中級 (⭐⭐):Notifierを使ってCartNotifierを追加/削除/更新と合計金額計算付きで実装し、2つの異なるページにカート状態を表示してください。
- チャレンジ (⭐⭐⭐):@riverpodコード生成を使ってAuth + Product + Cart Notifierを実装してください:ログイン後に商品を自動読み込み、カート追加後にバッジが自動更新されるようにします。