【Flutter】Riverpodの基礎知識メモ


## ■用途
状態管理を実現するためのアーキテクチャ
状態管理は煩雑なため、大規模開発では以下のアーキテクチャを利用する

- Bloc: Streamを活用したリアクティブな状態管理
- Redux: ActionCreator（ビジネスロジック） -> Action（状態への操作） -> Reducer（状態更新=Store更新）と役割が一方通行で処理を追いやすい
- Provider: 直感的な状態管理
- Riverpod: Providerの強化版

■メリット
- Provider = データの管理、提供者 / Consumer:データの使用者
と役割が明確に分かれているので状態管理がしやすい
- Cosumerはデータを監視することでProviderから自動でデータを受け取ることができる。
- アプリの状態を簡単にモック化することができ、テストがしやすい

## ■導入手順
プラグインの追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136142/ecebd763-4379-dad3-5c05-f56638a9b1e2.png)

pubspec.yamlにriverpod関連パッケージを追記し、Pub getを実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136142/5d2b9845-ceb0-6a24-5279-52efd11c625e.png)

※↑（コードの自動生成が不要なら以下3行は不要）

```yaml
  build_runner: ^2.4.6
  riverpod_generator: ^2.3.3
  riverpod_analyzer_utils: ^0.3.4
```
  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136142/84d40065-068b-0dc8-c237-0229573ab403.png)

## ■各モジュールの役割

■Provider

＜Provider＞
- 値を更新する必要のない定数などの受け渡しに使う

```dart
// Provider
final titleProvider = Provider<String>((ref) => 'sampleTitle');
// これと同じ
// final titleProvider = Provider<String>((ref) {
//   return 'sampleTitle';
// });

// 参照側（Consumer）
AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(ref.watch(titleProvider)),
),
```

＜StateProvider＞
- 値を変更する変数などの受け渡しに使う

```dart
// Provider
final countProvider = StateProvider<int>((ref) => 0);

// 更新（Consumer）
floatingActionButton: FloatingActionButton(
onPressed: () => ref.watch(countProvider.notifier).state++,
tooltip: 'Increment',
child: const Icon(Icons.add),
)

// 参照（Consumer）
Text(
  ref.watch(countProvider).toString(),
  style: Theme.of(context).textTheme.headlineMedium,
)
```

＜FutureProvider＞
- 非同期通信のロジックを実行するためのProvider

＜FutureProviderFamily＞
- FutureProviderに引数を渡すためのProvider
（引数を変えるたびにインスタンスが生成されるため非推奨）

＜AutoDisposeFutureProviderFamily＞
- FutureProviderFamilyのリソースを自動で開放してくれるProvider

＜riverpod_generatorパッケージについて＞

特徴：riverpod_generatorを使うと、独自Providerを自動生成してくれる
利用例：AutoDisposeFutureProviderFamilyを使う場合、引数は1つしか渡せないが、複数の引数を利用するよう独自定義できる

＜StreamProvider＞

- FirebaseStoreなどの更新を監視するために利用できる。
- 他のプロバイダで ref.watch を通じてストリームを監視することができる。
- AsyncValue により loading/error のステートを適切に処理することができる。
- ストリームから出力された直近の値をキャッシュしてくれる（途中で監視を開始しても最新の値を取得することができる）。
- StreamProvider をオーバーライドすることでテスト時のストリームを簡単にモックすることができる。

https://riverpod.dev/ja/docs/providers/stream_provider

＜StateNotifierProvider＞
StateProvider/FutureProvider/StreamProvider全てのユースケースに対応できる
https://zenn.dev/3ta/articles/fb2329ba2ab1dd

■Consumer

＜ConsumerWidget＞
- RiverpodでのStatelessWidgetに相当
- buildの引数は

```dart
Widget build(BuildContext context, WidgetRef ref)
```

- デメリット：変更があった際にConsumerWidget.buildが実行されてウィジェットツリーが再構築される

＜ConsumerStatefulWidget＞
- RiverpodでのStatelessWidgetに相当
- buildの引数は

```dart
// refはConsumerStatefulWidget内にrefが定義されているので、受け取らずにどこでも使える
Widget build(BuildContext context)
```

＜Comsumer＞
- Providerと値を送受信したいウィジェットをComsumerで囲んで使う
- 変更点があったらそのウィジェットだけがrebuild(再構築)される
- ConsumerWidgetやConsumerStatefulWidgetの中で利用しても有効。

```dart
class MyHomePage extends StatelessWidget {
	...
    title: Consumer(
      // builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(titleProvider),と同等
      builder: (context, ref, child) => Text(ref.watch(titleProvider),
      )
    ),
    ...
  floatingActionButton:
    Consumer(
      // builder: (BuildContext context, WidgetRef ref, Widget? child) => FloatingActionButton(と同等
      builder: (context, ref, child) => FloatingActionButton(
      onPressed: () => ref.watch(countProvider.notifier).state++,
      tooltip: 'Increment',
      child: const Icon(Icons.add),
    )
  ), 
```

- デメリット：Providerを使う箇所全てにConsumerで囲うのは手間がかかる。視認性、保守性が下がる

```dart
      // title: Text(ref.watch(titleProvider), )
      // ↑こう書いていたのを↓このようにConsumerで囲まなくてはいけない

    title: Consumer(
      // builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(titleProvider),と同等
      builder: (context, ref, child) => Text(ref.watch(titleProvider),
      )
    ),
```

■ref.watch/ref.readとselect
- ref.watch: 監視し、変更があったらそれを使ってるウィジェットをrebuildする。
-> Textの中でデータバインディングしているなど、監視が必要なときに利用

- ref.read: 単に読み込むだけで、そのウィジェットのrebuildも走らずに反映される。
-> onTapなど、イベント発生時に値を取得したいだけなら監視は不要。値を取得できればいいのでreadを使う

readとwatchでrebuildが走るタイミングを確認
```dart
	// watchを使うと該当部分の再描画のたびに以下ログが出力される
    Consumer(
        // builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(countProvider).toString(),と同等
        builder: (context, ref, child) {
          print("⭐️rebuild 1");
          return Text(
            ref.watch(countProvider).toString(),
            style: Theme.of(context).textTheme.headlineMedium,
          );
      }
    ),

	// readを使うと該当部分の再描画の際に以下ログが出力されない
    Consumer(
        // builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(countProvider).toString(),と同等
        builder: (context, ref, child) {
          print("⭐️rebuild 2");
          return Text(
            ref.read(countProvider).toString(),
            style: Theme.of(context).textTheme.headlineMedium,
          );
      }
    ),

```

- selectは、watchの中で監視する対象を絞り込める。余計なものを監視しないので、必要な項目が変更されたときだけrebuild走るようになる

selectの実装例
```dart
// selectなし: countUpだけじゃなく、countDownに変更があった場合もTextはrebuildされる
Text(ref.watch(countDataProvider).countUp.toString(),)

// selectあり: countDownに変更があった場合はTextはrebuildされない
Text(ref.watch(countDataProvider.state.select((value) => value.state.countUp)).toString(),)
```

※補足
```dart
// 旧参照処理
ref.watch(countProvider).state.toString();
// 新参照処理
ref.watch(countProvider).toString(),

// 旧更新処理
ref.watch(countProvider).state++;
// 新更新処理
ref.watch(countProvider.notifier).state++
```

■補足: freezedというパッケージの利用
- データクラスの自動生成
- copyWithなどの関数を自動で用意してくれる
- json形式の取り込みを簡単にできる

■サンプルコード

```mermaid
flowchart TB
  main["main()"] -->PS["ProviderScope(child: MyApp)"]
  PS--> vw1["ViewWidget1(ConsumerWidgetを継承)"] & vw2["ViewWidget2(ConsumerWidgetを継承)"]
    vw1 --> state1
    vw1 --> state2
    vw2 --> state1
    vw2 --> state2
    
subgraph Provider
  state1[状態インスタンス1]
  state2[状態インスタンス2]
end
```

```dart items_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

// 自動生成ありの場合
// ターミナルからflutter pub run build_runner buildコマンドで自動生成されるファイル名
// 拡張子前に.gをつける命名規則
// part 'items_provider.g.dart';
//
// @riverpod
// List<String> items(ItemsRef ref) {
//   return [
//     'アニメ',
//     'グルメ',
//     '不動産',
//     'ニュース',
//     'エンタメ',
//     '教育',
//     'ビジネス',
//     '自動車',
//     'バイク',
//     '観光',
//     '手芸',
//   ];
// }

// 自動生成なしの場合
final itemsProvider = Provider<List<String>>((ref) {
  return [
    'アニメ',
    'グルメ',
    '不動産',
    'ニュース',
    'エンタメ',
    '教育',
    'ビジネス',
    '自動車',
    'バイク',
    '観光',
    '手芸',
  ];
});
```

```dart favorites_provider.dart
// 自動生成なしの場合は必要
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 自動生成ありの場合は必要
// import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';

const _key = 'favorites';

// ダミーデータ
// @riverpod
// Future<Set<String>> favorites (FavoritesRef ref) async {
//   return {
//     'アニメ',
//     'グルメ',
//   };
// }

// 自動生成ありの場合
// part 'favorites_provider.g.dart';
// @riverpod
// class Favorites extends _$Favorites {
//   @override
//   FutureOr<Set<String>> build() async {
//     final sharedPreferences = await SharedPreferences.getInstance();
//     final favorites = sharedPreferences.getStringList(_key) ?? <String>[];
//     return Set.from(favorites);
//   }
//
//   Future<void> addItem(String item) async {
//     final set = state.value ?? {};
//     set.add(item);
//     await _saveSet(set);
//   }
//
//   Future<void> removeItem(String item) async {
//     final set = state.value ?? {};
//     set.remove(item);
//     await _saveSet(set);
//   }
//
//   Future<void> _saveSet(Set<String> set) async {
//     final sharedPreferences = await SharedPreferences.getInstance();
//     sharedPreferences.setStringList(_key, set.toList(growable: false));
//     state = AsyncData(set);
//   }
// }

// 自動生成なしの場合
final favoritesProvider = StateNotifierProvider<FavoritesNotifier, Set<String>>((ref) {
  return FavoritesNotifier();
});

class FavoritesNotifier extends StateNotifier<Set<String>> {
  FavoritesNotifier() : super({}) {
    loadFavorites(); // 初期化時にお気に入りを読み込む
  }

  Future<void> loadFavorites() async {
    final sharedPreferences = await SharedPreferences.getInstance();
    final favorites = sharedPreferences.getStringList(_key) ?? <String>[];
    state = Set.from(favorites);
  }

  Future<void> addItem(String item) async {
    final set = Set<String>.from(state);
    set.add(item);
    await _saveSet(set);
    state = set;
  }

  Future<void> removeItem(String item) async {
    final set = Set<String>.from(state);
    set.remove(item);
    await _saveSet(set);
    state = set;
  }

  Future<void> _saveSet(Set<String> set) async {
    final sharedPreferences = await SharedPreferences.getInstance();
    sharedPreferences.setStringList(_key, set.toList(growable: false));
    state = set;
  }
}

```

```dart main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'items/items_provider.dart';
import 'favorites/favorites_provider.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(itemsProvider);
    final favorites = ref.watch(favoritesProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('あなたの興味は？'),
      ),
      body: ListView.builder(
        itemBuilder: (context, index) {
          final item = items[index];
          // 自動生成ありの場合
          // final isFavorite = favorites.valueOrNull?.contains(item) ?? false;
          // 自動生成なしの場合
          final isFavorite = favorites.contains(item);
          return ListTile(
            leading: IconButton(
              onPressed: () {
                if (isFavorite) {
                  ref.read(favoritesProvider.notifier).removeItem(item);
                } else {
                  ref.read(favoritesProvider.notifier).addItem(item);
                }
              },
              icon: isFavorite
                  ? const Icon(Icons.favorite, color: Colors.pink)
                  : const Icon(Icons.favorite_outline),
            ),
            title: Text(item),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => ItemDetailPage(item: item),
                ),
              );
            },
          );
        },
        itemCount: items.length,
      ),
    );
  }
}

class ItemDetailPage extends ConsumerWidget {
  const ItemDetailPage({required this.item, super.key});

  final String item;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final favorites = ref.watch(favoritesProvider);
    // 自動生成ありの場合
    // final isFavorite = favorites.valueOrNull?.contains(item) ?? false;
    // 自動生成なしの場合
    final isFavorite = favorites.contains(item) ?? false;

    return Scaffold(
      appBar: AppBar(
        title: Text('$item'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              '$itemの詳細画面',
              style: TextStyle(fontSize: 24),
            ),
            IconButton(
                onPressed: () {
                  if (isFavorite) {
                    ref.read(favoritesProvider.notifier).removeItem(item);
                  } else {
                    ref.read(favoritesProvider.notifier).addItem(item);
                  }
                },
                icon: isFavorite
                    ? const Icon(
                        Icons.favorite,
                        size: 50,
                        color: Colors.pink,
                      )
                    : const Icon(
                        Icons.favorite_outline,
                        size: 50,
                      ))
          ],
        ),
      ),
    );
  }
}

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136142/d227a45d-8a0c-8388-ae12-29ff2c7d668a.png)