Riverpod、正しく使えてますか？(自戒)

## はじめに

こんにちは、今回はお馴染みのRiverpodのパッケージについて、改めて学んだことを共有したいと思います。

普段、何気なく使用しているパッケージだと思いますが、
最新のアップデートに追いつけてなかったり、
気付かぬうちにチーム内や個人内で変な癖がついてるなんてことがあるかもしれません。

この記事で復習がてらどなたかの為になれば幸いです！
なおこの記事はRiverpodの使い方を説明する記事ではないので、
使い方を知りたい方は以下の公式ドキュメントか
他のドキュメントを参照することをお勧めします。


上記は公式ドキュメントですので一度参照することをお勧めします。

もし何か間違いやご指摘があればコメント等頂けると幸いです。

## read,watchの使い分けを考える。

providerを取得する方法は三種類あります。

* ref.watch
* ref.listen
* ref.read

ここでは、watchとreadの使い分けについて言及したいと思います。

readは プロバイダの値を取得するのみで監視はしない点がwatchとの違いです。


* **onTapなどを非同期でアクションを呼び出している際には使用しない。**

非同期内のロジックで、watchを使用してproviderを呼び出すと、
非同期のタスクが終了する前に、UIのリビルドが走ってしまい、
古いデータが表示されてしまう可能性があるとのことです。

(非同期のロジックであるから必ず古いデータが表示されるわけではなし、
実際に挙動を確認して問題がなさそうなら、watchを使用しても問題なさそうと思いましたが、一応公式ドキュメントには、そのような記載がありました。)


* **initStateなどのStateのライフサイクル内では使用しない。**

initStateなどのライフサイクルでは、
Widgetがまだビルドされていない状態なため、

initState内でwatchの処理が走ると、
Widgetが構築されていないにも関わらず、
WIdgetツリーへのアクセスを要求してしまうので結果としてビルドエラーになります。


:::note warn
注意
今回は省略しましたが、Listenに関しても上記のwatchと同じく、state内、非同期内での使用は推奨されていません。
:::



* **readはbuild内では使用しない。**

どのようなソフトウェアでも**時間の経過や仕様変更で概要やコードに変更が出てくることは当たり前**に想定しなくてはいけません。

現状はプロバイダの状態変更をUIに反映させる必要がない場合でも、
今後の仕様変更でその変数の使われ方が変更になるかもしれません。
そういった際に、プロバイダの呼び出し方をreadからwatchから変更する手間がかかってしまいますし、変更し忘れなどでエラーにつながることがあります。

具体的な例をコードベースで見ていきましょう。

```dart

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

Widget build(BuildContext context, WidgetRef ref) {
  StateController<int> counter = ref.watch(counterProvider.notifier);
  //ここをreadに変更しても、widgetの呼び出し回数は変わらない。
  return ElevatedButton(
    onPressed: () => counter.state++,
    child: const Text('button'),
  );
}
```

上記のコードだと、counterという変数がreadだろうが、watchだろうが、
Widgetの呼び出される回数は一切変わりません。

パフォーマンス的に差異がないのであれば、readを積極的に採用する理由はなさそうですね。

## selectを使って、値を更新する条件を限定する。

```dart:
abstract class User {
  String get name;
  int get age;
}
```
```dart
Widget build(BuildContext context, WidgetRef ref) {
  String name = ref.watch(userProvider.select((user) => user.name));
  return Text(name);
}
```

上記のUserのクラスのデータをプロバイダで管理しようとしているケースを想定しましょう。

しかし、プロバイダを受け取る側は、nameのデータのみ必要でageのデータは不要な場合もあると思います。

この際に、selectを使用することで、不必要なデータの変化でUIの更新をするという無駄な処理を避けることが可能になります。

必要な場面では、selectを積極採用しましょう。

## プロバイダ修飾子

以下のプロバイダ修飾子を使うことによって、
プロバイダに対して、便利な機能を提供してくれます。

### familyについて

このfamilyは、プロバイダに引数を付与することによって、
その引数によって一意のプロバイダを作成することができます。
理解を深めるためにコードを見ていきましょう。

```dart
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
  return dio.get('http://my_api.dev/messages/$id');
});
```
上記のコードがfamily修飾子としてわかりやすい例だと思います。
messageFamilyという変数はfamily修飾子を使用しており、
String型のidという名前の引数をとっており、
引数のidが返り値に含まれている形になっています。
このようにidによって一意のプロバイダを制作することができます。

また、family修飾子がついたプロバイダを呼び出す際は、
以下のように引数をつける必要があります。

```dart
Widget build(BuildContext context, WidgetRef ref) {
  final response = ref.watch(messagesFamily('id'));
}
```

### autoDisposeについて


autoDispose修飾子は、特定のプロバイダを自動的に破棄するために使用される便利な修飾子です。
autoDisposeは、特定の画面のみプロバイダが
必要な場面などした際に有用で、
画面を閉じたり、不要になった際に、
プロバイダが破棄される仕組みになっています。

```dart
final counterProvider = Provider.autoDispose((ref) {
  return Counter();
});

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter Page'),
      ),
      body: Center(
        child: Consumer(
          builder: (context, watch, child) {
            final counter = watch(counterProvider);
            return Text('Count: ${counter.count}');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // ボタンが押されたらカウンターをインクリメント
          context.read(counterProvider).increment();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
```

上記の例では、CounterPageウィジェットが破棄されるとcounterProviderが自動的に解放されます。
しかし、autoDisposeを使用する際に注意しなければならないこともあります。

具体的には以下が挙げられます。

* **プロバイダが高コストな初期化処理を必要とする場合**

上記の場合、autoDisposeで修飾すると、Widgetが破棄されたタイミングでstateがdisposeするので、
大きなデータなどを読み込む必要がある際には、注意が必要です。

* **プロバイダが早すぎるタイミングで破棄される場合**

autoDispose修飾子を誤って適用した場合、
ウィジェットのライフサイクルよりも早いタイミングでプロバイダが破棄されることがあります。
プロバイダがまだ必要な場合にアクセスしようとすると、
エラーや未定義の動作が発生する可能性がありますので注意が必要です。





## 最後に

最後まで読んで頂きありがとうございます。
細かい部分が多くて大変ですが、flutterで開発する上でRiverpodはマストのパッケージですので、
頑張りましょう！
併せて、間違いやご指摘等あればコメント等いただけると幸いです。