Flutter: 状態管理 — Riverpod

状態はアプリの命の源です — 管理を誤ると「内出血」を引き起こします:データの不整合、リビルドストーム、メモリリーク。

📋 前提条件: 以下を先にマスターしている必要があります

1. このレッスンで学ぶこと


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のコア概念

100%
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を使用します。

📖 まとめ


📝 練習問題

  1. 基本 (⭐):StateProviderを使ってダークモード切り替えを実装し、ConsumerWidgetでAppBarに現在のモードを表示してください。
  2. 中級 (⭐⭐):Notifierを使ってCartNotifierを追加/削除/更新と合計金額計算付きで実装し、2つの異なるページにカート状態を表示してください。
  3. チャレンジ (⭐⭐⭐):@riverpodコード生成を使ってAuth + Product + Cart Notifierを実装してください:ログイン後に商品を自動読み込み、カート追加後にバッジが自動更新されるようにします。

← 前のレッスン | 次のレッスン →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%