Flutter Riverpod 入門まとめ|使い方・選び方・よくあるバグ
はじめに
Riverpod は Flutter の状態管理ライブラリです。
似た名前の provider パッケージとは別物です。
| パッケージ | import |
|---|---|
旧来の状態管理 (provider) |
package:provider/provider.dart |
| Riverpod | package:flutter_riverpod/flutter_riverpod.dart |
1. セットアップ
# pubspec.yaml
dependencies:
flutter_riverpod: ^2.x.x
アプリ全体を ProviderScope で囲む(これを忘れると動かない)。
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
2. Provider の選び方
| Provider | 用途 |
|---|---|
Provider |
他 Provider から計算する読み取り専用の値 |
StateProvider |
単純な値(bool, int など)を外から変更したい |
NotifierProvider |
add() / delete() など独自メソッドで state を操作したい |
AsyncNotifierProvider |
非同期処理(DB・API)を含む state |
StreamProvider |
リアルタイム更新(Firestore など) |
選び方のポイント
- 「他の Provider の値から計算できる」→
Provider - 「外からメソッドで操作したい」→
NotifierProvider - 「非同期が絡む」→
AsyncNotifierProvider
3. Widget 側の書き方
ConsumerWidget(StatelessWidget の代替)
class BalanceCard extends ConsumerWidget {
const BalanceCard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final balance = ref.watch(balanceProvider);
return Text('残高: $balance 円');
}
}
ConsumerStatefulWidget(StatefulWidget の代替)
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen> {
@override
Widget build(BuildContext context) {
// ref はそのまま使える(引数不要)
final balance = ref.watch(balanceProvider);
return Text('残高: $balance 円');
}
}
4. ref.watch と ref.read の使い分け
Riverpod で最も重要なルールです。
| 使う場所 | 用途 | |
|---|---|---|
ref.watch |
build() 内 |
値の変化を監視して再描画 |
ref.read |
コールバック内 | 一回だけ値を取得 / メソッドを呼ぶ |
@override
Widget build(BuildContext context) {
// ✅ build() 内は ref.watch
final balance = ref.watch(balanceProvider);
final transactions = ref.watch(transactionProvider);
return Column(
children: [
Text('残高: $balance 円'),
ElevatedButton(
// ✅ コールバック内は ref.read
onPressed: () {
ref.read(transactionProvider.notifier).add(newTransaction);
},
child: Text('追加'),
),
],
);
}
// ❌ コールバック内で ref.watch は NG(監視ライフサイクルが壊れる)
onPressed: () {
ref.watch(balanceProvider); // NG
}
コールバックに渡す場合
// ✅ notifier は安定したオブジェクトなので ref.read で渡してOK
TransactionList(
onDelete: ref.read(transactionProvider.notifier).delete,
)
5. Provider の書き方
読み取り専用(派生値)
他 Provider の値から計算できる場合は Provider を使います。
Notifier を使う必要はありません。
// ✅ 取引リストから残高を自動計算
final balanceProvider = Provider<int>((ref) {
final transactions = ref.watch(transactionProvider);
return transactions.fold(0, (sum, ts) => sum + ts.amount);
});
独自メソッドあり(NotifierProvider)
final transactionProvider =
NotifierProvider<TransactionProvider, List<Transaction>>(
TransactionProvider.new,
);
class TransactionProvider extends Notifier<List<Transaction>> {
@override
List<Transaction> build() => []; // ← ref.watch はここでのみ使える
void add(Transaction ts) {
state = [...state, ts]; // state への代入で Widget に通知される
}
void delete(int index) {
state = [...state]..removeAt(index);
}
}
Notifier 内での ref の使い方
class SomeNotifier extends Notifier<SomeState> {
@override
SomeState build() {
// ✅ build() 内は ref.watch OK
final other = ref.watch(otherProvider);
return SomeState(other);
}
void doSomething() {
// ❌ build() 以外で ref.watch は NG
// ✅ ref.read を使う
final other = ref.read(otherProvider);
}
}
6. よくあるバグ
state に代入していない
// ❌ 計算結果を捨てている(state は変わらない)
void updateBalance(int amount) => state + amount;
// ✅ state への代入が必要
void updateBalance(int amount) => state += amount;
複数の Provider を手動で同期させる
// ❌ add と updateBalance を別々に呼ぶ → ズレが起きる
ref.read(transactionProvider.notifier).add(ts);
ref.read(balanceProvider.notifier).updateBalance(amount);
// ✅ 派生 Provider にして自動計算させる
final balanceProvider = Provider<int>((ref) {
return ref.watch(transactionProvider).fold(0, (s, ts) => s + ts.amount);
});
// → transactionProvider が更新されると balanceProvider も自動で再計算
delete の実装ミス
// ❌ 指定インデックスの要素「だけ」残す(逆になっている)
void delete(int index) {
state = state.where((ts) => state[index] == ts).toList();
}
// ✅
void delete(int index) {
state = [...state]..removeAt(index);
}
7. 実務に向けた次のステップ
autoDispose(メモリリーク防止)
画面を閉じたときに Provider を自動破棄します。
final transactionProvider =
NotifierProvider.autoDispose<TransactionProvider, List<Transaction>>(
TransactionProvider.new,
);
family(パラメータ付き Provider)
IDなど引数によって異なるデータを取得したい場合。
final transactionByIdProvider = Provider.family<Transaction?, int>((ref, id) {
return ref.watch(transactionProvider).firstWhereOrNull((ts) => ts.id == id);
});
// 使う側
final transaction = ref.watch(transactionByIdProvider(42));
AsyncNotifier(非同期処理)
DB や API を使う場合は AsyncNotifier に切り替えます。
class TransactionProvider extends AsyncNotifier<List<Transaction>> {
@override
Future<List<Transaction>> build() async {
return await repository.fetchAll(); // DB・API から取得
}
Future<void> add(Transaction ts) async {
await repository.save(ts);
ref.invalidateSelf(); // 再取得
}
}
ProviderContainer(ユニットテスト)
Widget を使わずに Provider 単体でテストできます。
test('取引を追加すると残高が増える', () {
final container = ProviderContainer();
addTearDown(container.dispose);
container.read(transactionProvider.notifier).add(
Transaction(title: '給料', amount: 300000, date: DateTime.now()),
);
expect(container.read(balanceProvider), 300000);
});
まとめ
| やること | 使うもの |
|---|---|
| 監視して再描画 |
ref.watch(build内) |
| 一回だけ取得・操作 |
ref.read(コールバック内) |
| 計算できる値 | Provider |
| 独自メソッドで操作 | NotifierProvider |
| 非同期処理 | AsyncNotifierProvider |
| 画面破棄時に解放 | autoDispose |
| 引数付き | family |