FlutterでのRiverpod:基本的なProviderからRiverpod GeneratorまでプロのState管理

Development tutorial - IT technology blog
Development tutorial - IT technology blog

この記事はコードに直結します — 長い理論はありません。古いProviderパッケージを使っていてstate管理がどんどん複雑になっていると感じている?それとも新しいプロジェクトを始めるにあたって何を選べばいいか迷っている?Riverpod 2.xはその両方を解決します。見ていきましょう。

すぐ始める:Riverpodのインストールから5分で動かす

pubspec.yamlに依存関係を追加します:

dependencies:
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5

dev_dependencies:
  riverpod_generator: ^2.4.3
  build_runner: ^2.4.9

アプリ全体をProviderScopeでラップします — このステップは必須で、省略するとすぐにクラッシュします:

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

最初のproviderを作成してすぐに使います:

final counterProvider = StateProvider<int>((ref) => 0);

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: Text('増やす'),
        ),
      ],
    );
  }
}

ConsumerWidgetStatelessWidgetの代わりになります — これが古いProviderパッケージとの核心的な違いです。クイックスタートはここまで、より実践的な内容に進みましょう。

Providerの種類とその使い分け

StateProvider — シンプルなState

プリミティブ型(bool、int、String、enum)に限定して使います。Listやオブジェクトなどをここにねじこまないでください — それはNotifierProviderの仕事です:

final isDarkModeProvider = StateProvider<bool>((ref) => false);
final selectedTabProvider = StateProvider<int>((ref) => 0);

FutureProvider — API呼び出し、非同期ファイル読み込み

サーバーからデータをフェッチするときに最もよく使います。loading/errorのstateを自動で処理してくれるのが便利で — 余計なボイラープレートを書く必要がありません:

final userListProvider = FutureProvider<List<User>>((ref) async {
  final repo = ref.watch(userRepositoryProvider);
  return repo.fetchAll();
});

// Widget内
class UserList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncUsers = ref.watch(userListProvider);
    return asyncUsers.when(
      data: (users) => ListView.builder(
        itemCount: users.length,
        itemBuilder: (_, i) => ListTile(title: Text(users[i].name)),
      ),
      loading: () => CircularProgressIndicator(),
      error: (e, _) => Text('エラー: $e'),
    );
  }
}

NotifierProvider — 複雑なビジネスロジック

StateNotifierProviderはRiverpod 2.xから非推奨になりました — NotifierProviderがその代替です。シンプルなルール:stateを処理するロジックはすべてここに置き、widgetに詰め込まないこと:

class CartNotifier extends Notifier<List<CartItem>> {
  @override
  List<CartItem> build() => [];

  void addItem(CartItem item) {
    state = [...state, item];
  }

  void removeItem(String id) {
    state = state.where((item) => item.id != id).toList();
  }

  double get total => state.fold(0, (sum, item) => sum + item.price);
}

final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>(
  CartNotifier.new,
);

AsyncNotifierProvider — 副作用を伴う非同期State

ログイン、フォーム送信、チェックアウト — データの読み込みと副作用のトリガーが同時に必要な場面です。AsyncNotifierProviderはまさにこれを処理します:

class AuthNotifier extends AsyncNotifier<User?> {
  @override
  Future<User?> build() async => null;

  Future<void> login(String email, String password) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() =>
      ref.read(authRepositoryProvider).login(email, password),
    );
  }

  void logout() => state = const AsyncValue.data(null);
}

応用編:Riverpod Generator — 少ないコードで、少ないバグ

50Kラインのコードベースをリファクタリングした経験から最も高い授業料を払って学んだのは、始める前に十分なテストカバレッジが必要だということです。Riverpod Generatorはここで大いに役立ちます — ボイラープレートを自動生成し、リファクタリング時のリスクを減らし、手書きよりずっとコードの意図が明確になります。

開発中はbuild_runnerをウォッチモードで実行します:

dart run build_runner watch --delete-conflicting-outputs

アノテーションでproviderを書けば、残りはbuild_runnerが処理します:

// user_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'user_provider.g.dart'; // このファイルは自動生成されます

@riverpod
Future<List<User>> userList(UserListRef ref) async {
  return ref.watch(userRepositoryProvider).fetchAll();
}

// パラメータ付きprovider(familyの代替)
@riverpod
Future<User> userById(UserByIdRef ref, String id) async {
  return ref.watch(userRepositoryProvider).getById(id);
}

// クラスを使ったStateful provider
@riverpod
class Cart extends _$Cart {
  @override
  List<CartItem> build() => [];

  void add(CartItem item) => state = [...state, item];
  void remove(String id) => state = state.where((i) => i.id != id).toList();
}

keepAlive — widgetのdispose後にproviderをキャッシュ

Generatorのデフォルトでは、リスナーがいなくなるとproviderは自動でdisposeされます。configやユーザーセッションなどキャッシュが必要なデータにはkeepAlive: trueを使います:

@Riverpod(keepAlive: true)
Future<AppConfig> appConfig(AppConfigRef ref) async {
  return ConfigService().load(); // 一度だけ読み込まれる
}

本番プロジェクトからの実践的なTips

1. テストでproviderをオーバーライドする — Riverpodを選んだ理由

GetXやBLoCと比べて、Riverpodでのテストははるかにスッキリしています。モックフレームワークも不要。大げさなDIコンテナも不要:

testWidgets('ユーザー一覧を表示する', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        userListProvider.overrideWith((ref) async => [
          User(id: '1', name: 'テスト太郎'),
        ]),
      ],
      child: MaterialApp(home: UserList()),
    ),
  );

  await tester.pumpAndSettle();
  expect(find.text('テスト太郎'), findsOneWidget);
});

2. データの更新にref.invalidate()を使う

ElevatedButton(
  onPressed: () async {
    await ref.read(cartProvider.notifier).checkout();
    ref.invalidate(orderHistoryProvider); // 強制的に再フェッチ
    ref.invalidate(cartProvider);          // cartを初期状態にリセット
  },
  child: Text('決済する'),
)

3. callback内でwatchを使わない — よくあるミス

// ❌ 誤り — すぐに例外をスロー
ElevatedButton(
  onPressed: () {
    final user = ref.watch(userProvider); // callback内でwatchしてはいけない
  },
);

// ✅ 正しい — callback内ではreadを使う
ElevatedButton(
  onPressed: () {
    final user = ref.read(userProvider);
  },
);

4. listenManualで副作用を監視する

ConsumerStatefulWidgetでは、initStateref.listenManualを使ってstateの変化に反応します。widget全体の再ビルドをトリガーせずに:

@override
void initState() {
  super.initState();
  ref.listenManual(authProvider, (previous, next) {
    // auth stateが変わったときにナビゲート
    if (next.valueOrNull == null) {
      Navigator.pushReplacementNamed(context, '/login');
    }
  });
}

5. typeではなくfeatureでフォルダを整理する

lib/
  features/
    auth/
      providers/    ← auth_provider.dart + auth_provider.g.dart
      models/
      repositories/
      screens/
    cart/
      providers/
      models/
    ...

この構造はすべてのproviderを一つのフォルダにまとめるよりもはるかにスケールします。featureを削除したい?フォルダごと削除すれば完了 — あちこちに散らばったファイルを探し回る必要はありません。

古いProviderパッケージを使っている?feature単位で移行しましょう — 一括マイグレーションはやめてください。あの50Kラインのリファクタリングから学んだこと:少しずつ、テストをしっかり行い、全部まとめて変えてどこでバグが出たか分からなくなるより10倍ロールバックが楽になります。

Share: