💬

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 という感じ。

参考

https://zenn.dev/riscait/books/flutter-riverpod-practical-introduction/viewer/how-to-choose-a-provider

https://qiita.com/yuu1111main/items/285109b3197e1499e0a0

https://zenn.dev/wutchy_zenn/articles/a03dce8025fcf1

Discussion