RiverpodのFutureProviderと、invalidateの機能を整理してみる

Riverpodを使って、リポジトリからデータを取得する場合、StreamかFutureを使うことが多いかと思います。
すると、Providerを使う選択肢として`StreamProvider`か`FutureProvider`、`AsyncNotifierProvider`を使う選択肢になるかと思います。

`AsyncNotifierProvider`と`StreamProvider`は更新のタイミングがわかりやすいのですが、
`FutureProvider`はどのタイミングで更新されるのか興味があったため調べて整理してみます。

ちなみに、２０２３/01時点でのプロバイダーの種類は下記です。
* Provider
* StateProvider
* StateNotifierProvider
* FutureProvider
* StreamProvider
* ChangeNotifierProvider（非推奨）
* NotifierProvider
* AsyncNotifierProvider

# 画面遷移、画面一覧
ボトムナビゲーションバーからホーム画面とデータ一覧画面に遷移できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/51de0037-168a-2016-674d-0e15ed12fa27.png)

データ一覧画面では、データをリポジトリから取得して表示します。
データの取得は非同期で行うため、取得中はローディングを挟んで一覧が表示されます。
### ローディング
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/b50e12fa-64c9-7d2e-af73-d6618837453b.png)

### データ一覧画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/6b36abc2-133f-05fd-5488-20c32b82fa0f.png)


そして、データ一覧から各データをタップすると、データの詳細画面に遷移できます。

### データ詳細画面

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/0e044649-02d8-2d62-fdc0-0ebd795f0e28.png)


```dart
// データ一覧を取得するプロバイダー
final dataListProvider =
    FutureProvider<List<Data>>((_) => DataRepository.instance.fetchDataList());

// データを取得、更新するリポジトリ
class DataRepository {
  static final instance = DataRepository._();
  DataRepository._();
  final _dataList =
      List.generate(10, (index) => Data(id: '$index', name: "Name$index"));

  Future<List<Data>> fetchDataList() async {
    await Future.delayed(const Duration(seconds: 2));
    return _dataList;
  }

  void changeName(String id, String changedName) {
    final index = _dataList.indexWhere((element) => element.id == id);
    _dataList[index] = Data(id: id, name: changedName);
  }
}

// データクラス
class Data {
  String id;
  String name;

  Data({
    required this.id,
    required this.name,
  });
}
```

# 上の状態での動作
まず上のコードでの動作を整理します。
* 初回起動時、データ一覧画面に遷移したときに**ローディングが走る**
* ホーム画面に遷移し、再度データ一覧画面に遷移したときは**ローディングが走らない**
* データ一覧画面からデータ詳細画面に遷移して編集後、データ一覧画面に戻った場合、**一覧は更新されない**
* 編集後、ホーム画面に一度遷移し再度データ一覧に遷移すると、**ローディングは走らないが更新はされている**

最後の動作だけよくわかっていないのですが、更新はされているようなので問題ないと思います。
注目すべきは、データ詳細画面で編集後に、データ一覧画面に戻ったときに更新されていないことで、
最終的には、このタイミングで更新されるようにしたいと思います。

# autoDisposeを使う

下記のように、プロバイダー部分に`autoDispose`を使うとどうなるでしょうか？

```dart
// データ一覧を取得するプロバイダー
final dataListProvider =
    FutureProvider.autoDispose<List<Data>>((_) => DataRepository.instance.fetchDataList());
```

この場合、以下の動作になりました。
* 初回起動時、データ一覧画面に遷移したときに**ローディングが走る**
* ホーム画面に遷移し、再度データ一覧画面に遷移したときは**ローディングが走る**
* データ一覧画面からデータ詳細画面に遷移して編集後、データ一覧画面に戻った場合、**一覧は更新されない**
* 編集後、ホーム画面に一度遷移し再度データ一覧に遷移すると、**ローディングが走り、更新はされる**

データ詳細画面で編集後に、データ一覧画面に戻ったときに更新されていないこと以外はほぼ期待通りの動作です。
ただ、`autoDispose`を使った場合、ホーム画面からデータ一覧画面に遷移するたびにデータの再取得がされるので、この部分は仕様によって変わってくる部分かなと思います。
例えば、このデータの更新がこのユーザー一人だけによって行われる場合はデータ詳細画面からしか編集できないので、データ一覧画面に遷移する度にデータの再取得する必要はありません。
しかし、データ一覧がWeb上で他のユーザーにもリアルタイムで更新される場合はデータ一覧画面に遷移するタイミングで更新できた方が良いのかなと思います。

# invalidateを使ってみる

下記は、データの名前を変更するWidgetで、`onPressed`でリポジトリのデータを更新したあと、
`ref.invalidate(dataListProvider);`でプロバイダをリフレッシュしています。

```dart
ElevatedButton(
  onPressed: () {
    DataRepository.instance.changeName(widget.data.id, textController!.text);
    ref.invalidate(dataListProvider);
  },
  child: const Text("Change"),
),
```

そして、プロパイダー部分は`autoDispose`を外しています。

```dart
// データ一覧を取得するプロバイダー
final dataListProvider =
    FutureProvider<List<Data>>((_) => DataRepository.instance.fetchDataList());
```

この場合、以下の動作になりました。
* 初回起動時、データ一覧画面に遷移したときに**ローディングが走る**
* ホーム画面に遷移し、再度データ一覧画面に遷移したときは**ローディングが走らない**
* データ一覧画面からデータ詳細画面に遷移して編集後、データ一覧画面に戻った場合、**一覧は更新される
（上で更新されているため、以下の動作は意味がないため、割愛します。
（編集後、ホーム画面に一度遷移し再度データ一覧に遷移すると、ローディングが走らない、更新は上でされている）

# まとめ
どこでみたのか忘れたのですが、`FutureProvider`の使い所として、非同期で一回だけ取得するところで使うという記事を見ました。
上記の使い方でも問題ないですが、明示的に更新したい場合は`invalidate`を使っていくと良さそうです。