Riverpodを使用して開発をしていた時に学んだこと、学び直したこと

# Riverpodを使用して開発をしていた時に学んだこと、学び直したこと

## 目次
1. [はじめに](#はじめに)
2. [状態管理](#状態管理)
3. [Consumer](#consumer)
4. [Provider](#provider)
5. [あとがき](#あとがき)

# はじめに

flutterを使ってアプリ開発をしていた時に学んだこと、学び直したこと、開発中につまずいたことなどをまとめたものになります。

# 状態管理

watch readをよく開発で使用していたのでここをまず調べなおすことから始めました。
そのなかで、watchとreadだけでなくlistenがあること、どの場合にlistenを使うべきなどを学んだので学んだことを書いていきます。

### watch read listenとは？

watch read listenとは、指定したプロバイダの値の読み取りや更新、監視を行うことができる関数です。

>この関数は[WidgetRef](https://pub.dev/documentation/hooks_riverpod/latest/hooks_riverpod/WidgetRef-class.html)クラスのメンバ関数となっており、使用するためには条件がありますが後述するConsumerが必要となります。詳細は後述します。

#### watchとは

- プロバイダで公開されている値を取得し、監視を行うことができる
- 監視を行うことで、値に変更があった際にwatchを使用しているwidgetの再構築を行い画面に値の変化を反映することができる

```dart
final counter = ref.watch(counterProvider);
```

:::note warn
注意
widgetの更新範囲は、watchを使用しているwidgetのbuildメソッド全体となる為、使用方法に注意する必要がある
:::

#### readとは

- 使用した**時点**での値を取得することができる

``` dart
void onPressed() {
  final count = ref.read(counterProvider);
}
```

#### listenとは

- プロバイダで公開されている値を取得し、監視を行うことができる
- 監視を行うことができ、値の変更を検知することはできるが、widgetの再構築を**行わない**

```dart
ref.listen<int>(counterProvider, (previous, next) {
  if (next > 10) {
    showDialog(...);
  }
});
```

### 比較表

| 項目 | watch | read | listen |
|:-----------:|:-----------:|:------------:|:------------:|
| 値の監視 | ✅ | ❌ | ✅ |
| widgetの更新 | ✅ | ❌ | ❌ |

#### おまけ 

プロバイダーの一部分を部分的に監視し、その監視対象のメンバが変更された時にのみwidgetの更新を行いたい時は`select`関数が用意されています。

``` dart 
final age = ref.watch(personProvider.select((p) => p.age));
```

# Consumer

Consumerとは、部分再構築を行う為に使用されるWidgetでwatchやreadなどを利用する場合に必要となるrefオブジェクトを取得できます。

## Consumerの種類

Consumerには必要な箇所で必要な範囲でだけ`WidgetRef`使用するための`Consumer`と、StatelessWidgetのbuildメソッド全体で使用する為の`ConsumerWidget`、Statefulwidgetのbuildメソッド全体で使用する為の`ConsumerStatefulWidget`の3種類があります。

### 1. Consumer
ConsumerはStatelessWidgetやStatefulWidgetなどのrefオブジェクトを持たないwidget内の任意の範囲内でrefオブジェクトを使用したい時に使います。

``` dart
class ConsumerTestWidget extends StatelessWidget {
    const ConsumerTestWidget({super.key});

    @override
    Widget build(BuildContext context) => Consumer(
        builder: (context, WidgetRef ref,child) => Text(
            ref.watch(testProvider),
        ),
    );
}
``` 

### 2. ConsumerWidget

ConsumerWidgetはStatelessWidgetを拡張したwidgetで、上記のConsumerとは違いbuildメソッド内でrefオブジェクトを明示的な指定なしで使用できます。

``` dart
class ConsumerTestWidget extends ConsumerWidget {
    const ConsumerTestWidget({super.key});

    @override
    Widget build(BuildContext context) => Text(
        ref.watch(provider);
    );
}
``` 

### 3. ConsumerStatefulWidget

ConsumerStatefulWidgetはStatefulWidgetを拡張したwidgetです。
Stateだった場所がConsumerStateに置き換わっていたりなど、ConsumerWidgetに比べて使うために拡張前から変更するところが多いWidgetです。

``` dart
class ConsumerTestStatefulWidget extends ConsumerStatefulWidget {
    const ConsumerTestStatefulWidget({super.key});

    @override
    ConsumerState<ConsumerTestStatefulWidget> createState() 
        => ConsumerState<ConsumerTestStatefulWidget>;
 
}

class _ConsumerTestStatefulWidget 
    extends ConsumerState<ConsumerTestStatefulWidget>  {

    @override
    Widget build(BuildContext context, WidgetRef ref) => Text(
        ref.watch(provider);
    );
}
``` 

# Provider

Providerとは、Providerがラップしたステートの変更を監視するオブジェクトです。
グローバルにて宣言されるため、アプリの任意の箇所からアクセスすることが可能で、他のWidgetでの変更をrefオブジェクトを使用することで検出し、変更をWidgetに対して適応させることを可能にするオブジェクトで、Riverpodを使用した状態管理には欠かせない存在です。

## Providerの種類

RiverpodのリファレンスにはProviderは6種類あり、それぞれ用途が異なるとあります。

### 1. Provider

基本のProviderです。
値を同期的に生成し、計算結果をキャッシュするなどの用途で使われることから、リストデータのフィルタリング等で使われるようです。

```dart
final filteredListProvider = Provider<List<Item>>((ref) {
  final items = ref.watch(itemsProvider);
  return items.where((item) => item.isActive).toList();
});
```

### 2. StateProvider

外部から変更が可能なステートを公開するプロバイダです。
シンプルなステートを管理することに使うことを推奨されている為、バリデーションやカスタムクラスやList/Mapを使用するようなステートを管理する場合は`StateNotifierProvider`が推奨されています。
主にドロップダウンの更新や直近のデータに対しての加算処理などの簡易的な値の更新を行う場合に使用されます。

``` dart
/// 例
final counterProvider = StateProvider<int>((ref) => 0);

ref.read(counterProvider.notifier).update((state) => state + 1);
```

### 3. StateNotifierProvider

StateNotifierを監視し、公開するためのプロバイダです。
ユーザ操作などによって状態を管理するためのソリューションとしてRiverpodが推奨しています。
このプロバイダが公開するステートは**イミュータブル**（不変）なステートであるため、外部からはステートの値を直接変更することは**できません**。
ステートの値を変更する場合は変更する為のメンバ関数を用意することで変更が可能です。（簡単にいうとカプセル化されたオブジェクトです。）

```dart
class TodosNotifier extends StateNotifier<List<Todo>> {
  TodosNotifier() : super([]);
  
  void add(Todo todo) {
    state = [...state, todo];
  }
  
  void remove(String id) {
    state = state.where((todo) => todo.id != id).toList();
  }
}

final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
  return TodosNotifier();
});
```

### 4. FutureProvider

非同期操作が可能なProviderです。
errorとloadingのステートを処理することができる為、APIの呼び出しなどの非同期の処理のステータスによって表示するWidgetを自動で更新したりすることができる。

``` dart
/// 例
Widget build(BuildContext context, WidgetRef ref) {
  final config = ref.watch(fetchConfigurationProvider);

  return switch (config) {
    /// 異常時
    AsyncError(:final error) => Text('Error: $error'),
    /// 正常時
    AsyncData(:final value) => Text(value.host),
    /// 処理中
    _ => const CircularProgressIndicator(),
  };
}
```

### 5. StreamProvider

FutureProviderのStream版です。
Stream自体が値の更新を監視する性質を持つが、StreamProviderを使用することで、ref.watchを使用したwidgetの更新や、FutureProvider同様にerrorやloadingのステートごとに処理を変えることができる。
公式では、ライブチャットのチャット画面での、データ受信時の自動更新が例としてあげられていました。
（過去に個人制作でチャットアプリを作った時はここの処理に大変苦しめられた記憶があるので面白かったです。）

```dart
final messagesProvider = StreamProvider<List<Message>>((ref) {
  return FirebaseFirestore.instance
      .collection('messages')
      .snapshots()
      .map((snapshot) => snapshot.docs.map((doc) => Message.fromDoc(doc)).toList());
});
```

### 6. ChangeNotifierProvider（非推奨）

ChangeNotifierProviderはriverpod公式が非推奨としているプロバイダです。
理由としてはstateが**ミュータブル**（可変）であり、stateを直接変更することができ、どこで変更されたかの追跡や、意図しないstateの変更が起こりうるため、StateNotifierProviderを使用することを推奨しています。

```dart
class Counter extends ChangeNotifier {
  int _count = 0;
  
  void increment() {
    _count++;
    notifyListeners();
  }
}

```

# あとがき

## 参考リソース

- [Riverpod公式ドキュメント](https://riverpod.dev/)

## おわりに

開発中に学んだことや、この記事を書く上で改めて学び直したことを書いていきました。
開発中はgeneratorを使用していたので、意識していなかったことも知れたので、やってよかったと思います。
次はdartのバックエンドについて記事を書いていけたらと考えています。（公開予定はアドカレ）