💬
Riverpod:Provider / Widget / refメソッドの特徴や使い分けをまとめる
はじめに
Riverpod は、Flutter向けの強力な状態管理ライブラリです。
Riverpod には様々 provider やProviderと連携するための Widgetや、ref メソッドが提供されています。
- Provider - 状態を保持・提供する様々な種類のコンテナ
- Widget - Providerと連携するためのUI
- refメソッド - Providerの値にアクセスし操作するための手段
私自身よく分かっていない部分もあったので備忘録がてらまとめつつ、この記事でこれらの要素の特徴と使い分けを説明します。
各Providerの違い
1. Provider
特徴
- 基本的なProvider
- 一度定義すると外部から変更できない(読み取り専用)
- 他のProviderの値を参照して加工した値を保持できる
例
// 買い物リストの合計金額を計算するProvider
final cartItemsProvider = Provider<List<CartItem>>((ref) => [
CartItem(name: 'りんご', price: 150, quantity: 2),
CartItem(name: 'バナナ', price: 100, quantity: 3),
]);
// 合計金額を計算するProvider(読み取り専用)
final totalPriceProvider = Provider<int>((ref) {
final items = ref.watch(cartItemsProvider);
return items.fold(0, (sum, item) => sum + (item.price * item.quantity));
});
2. StateProvider
特徴
- 外部から直接値を変更できる
- シンプルな状態管理に適している
- イミュータブルな状態を提供
例
// 買い物リストの表示フィルター(すべて/未購入のみ)
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.all);
// 使用例
Consumer(
builder: (context, ref, child) {
final filterType = ref.watch(filterTypeProvider);
return SegmentedButton<FilterType>(
segments: [
ButtonSegment(value: FilterType.all, label: Text('すべて')),
ButtonSegment(value: FilterType.active, label: Text('未購入のみ')),
],
selected: {filterType},
onSelectionChanged: (newSelection) {
ref.read(filterTypeProvider.notifier).state = newSelection.first;
},
);
},
)
3. StateNotifierProvider
特徴
- StateNotifierクラスを監視するProvider
例
// 買い物リストの状態
class ShoppingListState {
final List<ShoppingItem> items;
ShoppingListState({required this.items});
ShoppingListState copyWith({List<ShoppingItem>? items}) {
return ShoppingListState(items: items ?? this.items);
}
}
// 買い物リストの操作を管理するStateNotifier
class ShoppingListNotifier extends StateNotifier<ShoppingListState> {
ShoppingListNotifier() : super(ShoppingListState(items: []));
void addItem(ShoppingItem item) {
state = state.copyWith(items: [...state.items, item]);
}
void removeItem(String id) {
state = state.copyWith(
items: state.items.where((item) => item.id != id).toList(),
);
}
void togglePurchased(String id) {
state = state.copyWith(
items: state.items.map((item) {
if (item.id == id) {
return item.copyWith(isPurchased: !item.isPurchased);
}
return item;
}).toList(),
);
}
}
// Provider定義
final shoppingListProvider = StateNotifierProvider<ShoppingListNotifier, ShoppingListState>((ref) {
return ShoppingListNotifier();
});
// 使用例
Consumer(
builder: (context, ref, child) {
final shoppingList = ref.watch(shoppingListProvider).items;
return ListView.builder(
itemCount: shoppingList.length,
itemBuilder: (context, index) {
final item = shoppingList[index];
return ListTile(
title: Text(item.name),
subtitle: Text('${item.price}円 × ${item.quantity}'),
trailing: Checkbox(
value: item.isPurchased,
onChanged: (_) => ref.read(shoppingListProvider.notifier).togglePurchased(item.id),
),
onLongPress: () => ref.read(shoppingListProvider.notifier).removeItem(item.id),
);
},
);
},
)
4. FutureProvider
特徴
- Future型のプロバイダー
- 非同期処理の結果を提供する
- AsyncValueを返し、loading/error/dataの状態を扱える
- APIからのデータ取得やユーザー情報取得、設定の読み込みとかに使えそう
例
// レシピAPIからデータを取得するFutureProvider
final recipesProvider = FutureProvider<List<Recipe>>((ref) async {
final apiClient = ref.watch(apiClientProvider);
// APIからレシピ一覧を取得
return await apiClient.fetchRecipes();
});
// 使用例
Consumer(
builder: (context, ref, child) {
final recipesAsync = ref.watch(recipesProvider);
return recipesAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('レシピの取得に失敗しました: $error')),
data: (recipes) => ListView.builder(
itemCount: recipes.length,
itemBuilder: (context, index) {
final recipe = recipes[index];
return RecipeCard(
title: recipe.title,
imageUrl: recipe.imageUrl,
cookingTime: recipe.cookingTime,
onTap: () => Navigator.pushNamed(
context,
'/recipe-detail',
arguments: recipe.id,
),
);
},
),
);
},
)
5. StreamProvider
特徴
- Stream型のプロバイダーなので、常に監視し状態が変われば最新の値を提供する
- リアルタイムデータの監視に適している
- FutureProviderと同様にAsyncValueを返す
- リアルタイムデータベース(Firebase等)の監視や継続的に更新されるデータの監視をしたい時、チャットメッセージ、位置情報の更新とかに使えそう
例
// Firestoreからリアルタイムで買い物リストを取得するStreamProvider
final shoppingListStreamProvider = StreamProvider<List<ShoppingItem>>((ref) {
final firestore = FirebaseFirestore.instance;
final userId = ref.watch(currentUserIdProvider);
return firestore.collection('users')
.doc(userId)
.collection('shopping_lists')
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => ShoppingItem.fromFirestore(doc))
.toList());
});
// 使用例
Consumer(
builder: (context, ref, child) {
final shoppingListAsync = ref.watch(shoppingListStreamProvider);
return shoppingListAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('買い物リストの取得に失敗しました: $error')),
data: (items) => items.isEmpty
? Center(child: Text('買い物リストが空です'))
: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ShoppingItemTile(
item: item,
onToggle: () => ref.read(shoppingServiceProvider).toggleItemStatus(item.id),
onDelete: () => ref.read(shoppingServiceProvider).removeItem(item.id),
);
},
),
);
},
)
6. NotifierProvider (Riverpod 2.0以降)
特徴
- StateNotifierProviderの後継(用途は同じ)
- コード生成と組み合わせて使用できる
- より簡潔な記述が可能
例
// TODOリストを管理するNotifier
class TodosNotifier extends Notifier<List<Todo>> {
List<Todo> build() {
return []; // 初期状態は空のTODOリスト
}
void addTodo(String title) {
final newTodo = Todo(
id: DateTime.now().toString(),
title: title,
completed: false,
);
state = [...state, newTodo];
}
void toggleTodo(String id) {
state = [
for (final todo in state)
if (todo.id == id)
todo.copyWith(completed: !todo.completed)
else
todo,
];
}
void removeTodo(String id) {
state = state.where((todo) => todo.id != id).toList();
}
}
// Provider定義
final todosProvider = NotifierProvider<TodosNotifier, List<Todo>>(() {
return TodosNotifier();
});
// 使用例
class TodoListScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todosProvider);
return Scaffold(
appBar: AppBar(title: Text('TODOリスト')),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return CheckboxListTile(
title: Text(todo.title),
value: todo.completed,
onChanged: (_) => ref.read(todosProvider.notifier).toggleTodo(todo.id),
secondary: IconButton(
icon: Icon(Icons.delete),
onPressed: () => ref.read(todosProvider.notifier).removeTodo(todo.id),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) => NewTodoDialog(
onAdd: (title) => ref.read(todosProvider.notifier).addTodo(title),
),
);
},
),
);
}
}
7. AsyncNotifierProvider (Riverpod 2.0以降)
特徴
- 非同期処理を扱うNotifierProvider
- FutureProviderとStateNotifierProviderの良いところを組み合わせたもの
- 非同期データの取得と操作が必要な場合やユーザー情報の取得と更新、データの同期の時などに使う
例
// オンラインショッピングカートを管理するAsyncNotifier
class CartNotifier extends AsyncNotifier<List<CartItem>> {
Future<List<CartItem>> build() async {
// 初期データをAPIから取得
final cartService = ref.watch(cartServiceProvider);
return await cartService.fetchCartItems();
}
Future<void> addToCart(Product product, int quantity) async {
// 現在の状態を取得(loading中の場合は処理をスキップ)
final currentCart = state.valueOrNull;
if (currentCart == null) return;
// 楽観的UI更新(即座にUIに反映)
final newItem = CartItem(
id: DateTime.now().toString(),
productId: product.id,
name: product.name,
price: product.price,
quantity: quantity,
imageUrl: product.imageUrl,
);
state = AsyncData([...currentCart, newItem]);
try {
// バックエンドに保存
final cartService = ref.read(cartServiceProvider);
await cartService.addItemToCart(product.id, quantity);
} catch (e) {
// エラー時は元の状態に戻す
state = AsyncData(currentCart);
// エラーを通知
state = AsyncError(e, StackTrace.current);
}
}
Future<void> updateQuantity(String itemId, int newQuantity) async {
final currentCart = state.valueOrNull;
if (currentCart == null) return;
// 楽観的UI更新
state = AsyncData([
for (final item in currentCart)
if (item.id == itemId)
item.copyWith(quantity: newQuantity)
else
item,
]);
try {
// バックエンドに保存
final cartService = ref.read(cartServiceProvider);
await cartService.updateCartItemQuantity(itemId, newQuantity);
} catch (e) {
// エラー時は元の状態に戻す
state = AsyncData(currentCart);
state = AsyncError(e, StackTrace.current);
}
}
Future<void> removeFromCart(String itemId) async {
final currentCart = state.valueOrNull;
if (currentCart == null) return;
// 楽観的UI更新
state = AsyncData(currentCart.where((item) => item.id != itemId).toList());
try {
// バックエンドに保存
final cartService = ref.read(cartServiceProvider);
await cartService.removeCartItem(itemId);
} catch (e) {
// エラー時は元の状態に戻す
state = AsyncData(currentCart);
state = AsyncError(e, StackTrace.current);
}
}
}
// Provider定義
final cartProvider = AsyncNotifierProvider<CartNotifier, List<CartItem>>(() {
return CartNotifier();
});
// 使用例
class CartScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final cartAsync = ref.watch(cartProvider);
return Scaffold(
appBar: AppBar(title: Text('ショッピングカート')),
body: cartAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('カートの読み込みに失敗しました: $error')),
data: (cartItems) => cartItems.isEmpty
? Center(child: Text('カートは空です'))
: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: cartItems.length,
itemBuilder: (context, index) {
final item = cartItems[index];
return CartItemTile(
item: item,
onQuantityChanged: (newQuantity) {
ref.read(cartProvider.notifier).updateQuantity(item.id, newQuantity);
},
onRemove: () {
ref.read(cartProvider.notifier).removeFromCart(item.id);
},
);
},
),
),
CartSummary(items: cartItems),
CheckoutButton(onPressed: () {/* チェックアウト処理 */}),
],
),
),
);
}
}
8. (非推奨)ChangeNotifierProvider
特徴
- ChangeNotifierクラスを監視するProvider
- ミュータブルな状態管理
注意
代わりにStateNotifierProviderを使用することが推奨されています。
Riverpodで使用するWidget
1. ConsumerWidget
特徴
- StatelessWidgetの代わりに使用
- buildメソッドにWidgetRefが提供される
- Providerの値を監視して再ビルドできる
- 状態を持たないUI要素でProviderの値を使用したい場合に使う
例
class ProductListScreen extends ConsumerWidget {
const ProductListScreen({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
final filterType = ref.watch(filterTypeProvider);
// フィルタリングされた商品リスト
final filteredProducts = products.where((product) {
if (filterType == FilterType.all) return true;
if (filterType == FilterType.onSale) return product.isOnSale;
return false;
}).toList();
return Scaffold(
appBar: AppBar(title: Text('商品一覧')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
),
itemCount: filteredProducts.length,
itemBuilder: (context, index) {
final product = filteredProducts[index];
return ProductCard(
product: product,
onAddToCart: () {
ref.read(cartProvider.notifier).addToCart(product, 1);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name}をカートに追加しました')),
);
},
);
},
),
);
}
}
2. ConsumerStatefulWidget
特徴
- StatefulWidgetとStateの代わりに使用
- ConsumerStateクラスにWidgetRefが提供される
- ライフサイクルメソッド内でもProviderにアクセス可能
- 状態を持つUI要素でProviderの値を使用したい場合に使う
例
class RecipeTimerWidget extends ConsumerStatefulWidget {
final String recipeId;
const RecipeTimerWidget({Key? key, required this.recipeId}) : super(key: key);
ConsumerState<RecipeTimerWidget> createState() => _RecipeTimerWidgetState();
}
class _RecipeTimerWidgetState extends ConsumerState<RecipeTimerWidget> {
Timer? _timer;
int _remainingSeconds = 0;
bool _isRunning = false;
void initState() {
super.initState();
// initStateでProviderにアクセス
final recipe = ref.read(recipeDetailProvider(widget.recipeId)).value;
if (recipe != null) {
_remainingSeconds = recipe.cookingTimeMinutes * 60;
}
}
void _startTimer() {
setState(() {
_isRunning = true;
});
_timer = Timer.periodic(Duration(seconds: 1), (_) {
setState(() {
if (_remainingSeconds > 0) {
_remainingSeconds--;
} else {
_stopTimer();
// 調理完了をProviderに通知
ref.read(cookingHistoryProvider.notifier).addCompletedRecipe(widget.recipeId);
}
});
});
}
void _stopTimer() {
_timer?.cancel();
setState(() {
_isRunning = false;
});
}
void _resetTimer() {
_stopTimer();
setState(() {
final recipe = ref.read(recipeDetailProvider(widget.recipeId)).value;
if (recipe != null) {
_remainingSeconds = recipe.cookingTimeMinutes * 60;
}
});
}
void dispose() {
_timer?.cancel();
super.dispose();
}
Widget build(BuildContext context) {
final minutes = _remainingSeconds ~/ 60;
final seconds = _remainingSeconds % 60;
return Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'調理タイマー',
style: Theme.of(context).textTheme.headline6,
),
SizedBox(height: 16),
Text(
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}',
style: Theme.of(context).textTheme.headline4,
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _isRunning ? null : _startTimer,
child: Text('開始'),
),
ElevatedButton(
onPressed: _isRunning ? _stopTimer : null,
child: Text('一時停止'),
),
ElevatedButton(
onPressed: _resetTimer,
child: Text('リセット'),
),
],
),
],
),
),
);
}
}
3. Consumer
特徴:
- WidgetツリーのサブセットだけをRiverpodに接続するためのWidget
- buildメソッドにWidgetRefが提供される
- 必要な部分だけを再ビルドできる
- 大きなWidgetツリーの一部だけをProviderの変更に応じて再ビルドしたい場合に使う
例
class RestaurantDetailScreen extends StatelessWidget {
final String restaurantId;
const RestaurantDetailScreen({Key? key, required this.restaurantId}) : super(key: key);
Widget build(BuildContext context) {
// このWidgetは再ビルドされない
return Scaffold(
appBar: AppBar(title: Text('レストラン詳細')),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 静的なヘッダー画像(再ビルド不要)
RestaurantHeaderImage(restaurantId: restaurantId),
// レストラン基本情報(再ビルド不要)
RestaurantBasicInfo(restaurantId: restaurantId),
// お気に入りボタン(状態変更時のみ再ビルド)
Consumer(
builder: (context, ref, child) {
final isFavorite = ref.watch(favoriteRestaurantsProvider)
.contains(restaurantId);
return ListTile(
leading: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : null,
),
title: Text(isFavorite ? 'お気に入り登録済み' : 'お気に入りに追加'),
onTap: () {
if (isFavorite) {
ref.read(favoriteRestaurantsProvider.notifier)
.removeFromFavorites(restaurantId);
} else {
ref.read(favoriteRestaurantsProvider.notifier)
.addToFavorites(restaurantId);
}
},
);
},
),
// メニュー一覧(データ取得時のみ再ビルド)
Consumer(
builder: (context, ref, child) {
final menuItemsAsync = ref.watch(
restaurantMenuProvider(restaurantId)
);
return menuItemsAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (err, stack) => ErrorDisplay(message: 'メニューの取得に失敗しました'),
data: (menuItems) => MenuItemsList(items: menuItems),
);
},
),
// レビュー一覧(静的部分)
Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'レビュー',
style: Theme.of(context).textTheme.headline6,
),
),
// レビュー一覧(データ取得時のみ再ビルド)
Consumer(
builder: (context, ref, child) {
final reviewsAsync = ref.watch(
restaurantReviewsProvider(restaurantId)
);
return reviewsAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (err, stack) => ErrorDisplay(message: 'レビューの取得に失敗しました'),
data: (reviews) => reviews.isEmpty
? Center(child: Text('まだレビューがありません'))
: ReviewsList(reviews: reviews),
);
},
),
],
),
),
);
}
}
4. HookConsumerWidget
特徴
- flutter_hooks と Riverpod を組み合わせて使用
- HookWidget と ConsumerWidget の機能を統合
- Hooks を使用しつつ Provider にもアクセスしたい場合に使う
例
class RecipeSearchWidget extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// Hooksを使用して検索フィールドの状態を管理
final searchController = useTextEditingController();
final debounce = useRef<Timer?>(null);
// 検索テキスト変更時のデバウンス処理
useEffect(() {
searchController.addListener(() {
// 前のタイマーをキャンセル
debounce.value?.cancel();
// 新しいタイマーを設定(デバウンス処理)
debounce.value = Timer(Duration(milliseconds: 500), () {
// 検索クエリをProviderに設定
ref.read(recipeSearchQueryProvider.notifier).state = searchController.text;
});
});
return () {
debounce.value?.cancel();
searchController.dispose();
};
}, [searchController]);
// 検索結果をProviderから取得
final searchResultsAsync = ref.watch(recipeSearchResultsProvider);
return Column(
children: [
// 検索フィールド
Padding(
padding: EdgeInsets.all(16.0),
child: TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'レシピを検索...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
),
suffixIcon: searchController.text.isNotEmpty
? IconButton(
icon: Icon(Icons.clear),
onPressed: () {
searchController.clear();
ref.read(recipeSearchQueryProvider.notifier).state = '';
},
)
: null,
),
),
),
// 検索結果
Expanded(
child: searchResultsAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(
child: Text('検索中にエラーが発生しました: $err'),
),
data: (results) {
if (results.isEmpty && searchController.text.isNotEmpty) {
return Center(child: Text('検索結果が見つかりませんでした'));
}
if (results.isEmpty) {
return Center(child: Text('レシピを検索してください'));
}
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
final recipe = results[index];
return RecipeListItem(
recipe: recipe,
onTap: () => Navigator.pushNamed(
context,
'/recipe-detail',
arguments: recipe.id,
),
);
},
);
},
),
),
],
);
}
}
まとめ
状態管理の複雑さや非同期処理の有無などに応じて、各Providerの選択基準はこんな感じかも
RiverpodのProvider選定基準
用途 | Provider | 特徴 |
---|---|---|
読み取り専用の値 | Provider | 他のProviderから派生した値や計算結果 |
シンプルな状態管理 | StateProvider | 単純な値(int, bool, String)の管理 |
複雑な状態管理 | StateNotifierProvider / NotifierProvider | 複数の関連する値やロジックを含む状態管理 |
非同期データの取得 | FutureProvider | 一度だけ取得する非同期データ |
リアルタイムデータ | StreamProvider | 継続的に更新される非同期データ |
非同期データ + 状態管理 | AsyncNotifierProvider | 非同期データの取得と操作が必要な場合 |
Widgetの選択基準
用途 | Widget | 特徴 |
---|---|---|
状態を持たないUI | ConsumerWidget | StatelessWidgetの代替 |
状態を持つUI | ConsumerStatefulWidget | StatefulWidgetの代替 |
部分的な再ビルド | Consumer | 特定の部分だけを再ビルド |
Hooks + Riverpod | HookConsumer | flutter_hooksとRiverpodを併用可能 |
refのメソッド比較
用途 | メソッド | 特徴 |
---|---|---|
一度だけの読み取り | ref.read |
値を一度だけ読み取り、変更を監視しない。イベントハンドラ内で使用(onPressed , onTap など) |
値の監視 | ref.watch |
値の変更を監視し、変更があればWidgetを再ビルド。build 関数内で使用• 常に最新の値を反映 |
値の変更時に処理を実行 | ref.listen |
値の変更を監視し、変更があった時に特定のコールバックを実行。状態変化に応じたナビゲーションやダイアログ表示などに使用。build関数内で使用 |
ref.read
が Future ref.watch
が Stream という感じ。
参考
Discussion