【Flutter/Riverpod】画面遷移時の引数をProviderScopeのoverrideで設定する

この記事は ウェブクルー Advent Calendar 2022 22日目の記事です。
昨日は @tatsuyanamiki さんの「[PageSpeed Insightsで指摘されがちな改善提案と対策について](https://qiita.com/tatsuyanamiki/items/f8ffc604060e238170fb)」でした。

# はじめに

ウェブクルーでは、今年度よりアプリ開発準備室という、モバイルアプリ開発のスキル向上や、
モバイルアプリ開発組織の拡大に向けたナレッジ構築、企画開発をしていくチームを発足しました。

その中で採用した技術の１つに、モバイルアプリフレームワークの **Flutter** があります。
今回は、そのFlutterアプリ開発のスキル向上の際に培った技術の中で、
Riverpodの機能を活用した画面遷移で試した方法を記事にまとめようと思います。
なお、Flutter環境構築やRiverpodについての説明は割愛します。

# ページ間で引数を渡す
Flutterの画面遷移で調べていて、よく見かけた引数を渡す実装は、
下記の実装例のようにページ間で引数を渡していることが多かったです。

文字列 `testId` を画面遷移時に渡す場合：

画面遷移先では `testId` を受け取り、
受け取った `testId` を使ってViewModelなどに用意した関数を実行します。

```dart
class TestPage extends HookConsumerWidget {
  final String testId;
  const TestPage({Key? key, required this.testId}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(testViewModelProvider);
    final testViewModel = ref.watch(testViewModelProvider.notifier);

    useEffect(() {
      Future(() {
        testViewModel.fetchByTestId(testId);
      });
      return null;
    }, []);
    // ~~~ 省略 ~~~
  }
}
```

画面遷移元では、`onTap` や `onPressed` の処理で引数 `testId` を渡しています。

```dart
Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) {
      return TestPage(testId: data.testId);
    },
  ),
);
```

# ProviderScope override

上記の実装例だと、画面遷移元から渡された `testId` を
画面遷移先のMVVMでいうViewから引き回して操作するような実装になっています。
引数の数が増えたり呼び出す関数が増えると、View内の実装や定義も煩雑になってきます。

そこで、 `ProviderScope` の `override` を活用した実装です。

https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/ProviderScope-class.html

`ProviderScope` の `override` を活用すると、
ページ間で引数を渡すことなく、 `Provider` を上書きして引数を設定することができます。

# Provider.family

`ProviderScope` の `override` を活用する上で、
`Provider.family` についても知っておく必要があります。

https://riverpod.dev/ja/docs/concepts/modifiers/family/

`Provider.family` 修飾子を付けて `Provider` を作成すると、
`Provider` に引数を設定することができるようになります。

この `Provider.family` を `override` することで、
ページ間で引数を渡すことなく、 `Provider` に引数を渡すことができます。

# 実装例

`Provider` と `Provider.family` を用意します。
実装例ではMVVMでいうViewModelにあたる部分に実装しています。

```dart
final testViewModelProviderFamily =
  StateNotifierProvider.autoDispose.family<TestViewModel, AsyncValue<TestState>, String>(
    (ref, testId) => TestViewModel(testId, ref: ref));

final testViewModelProvider =
  StateNotifierProvider.autoDispose<TestViewModel, AsyncValue<TestState>>(
    (ref) => throw UnimplementedError());

class TestViewModel extends StateNotifier<AsyncValue<TestState>> {
  final AutoDisposeStateNotifierProviderRef _ref;
  final String testId;

  TestViewModel(this.testId, {required AutoDisposeStateNotifierProviderRef ref})
    : _ref = ref,
      super(const AsyncLoading());
  // ~~~ 省略 ~~~
}
```

画面遷移元の `onTap` や `onPressed` の処理で `Provider.family` を `override` します。
この際、ページ間で引数を渡す必要はないので、 `child: const TestPage()` となります。

```dart
Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) => ProviderScope(
      overrides: [
        testViewModelProvider
          .overrideWithProvider(testViewModelProviderFamily(data.testId))
      ],
      child: const TestPage()
    )
  )
)
```

画面遷移後は下記のように参照できます。
画面遷移前の `ProviderScope` の `override` で引数 `testId` を渡しているので、
画面遷移後のページ側では引数を意識することなく記述ができます。

```dart
class TestPage extends HookConsumerWidget {
  const TestPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(testViewModelProvider);
    final testViewModel = ref.watch(testViewModelProvider.notifier);

    useEffect(() {
      Future(() {
        testViewModel.fetchByTestId();
      });
      return null;
    }, []);
    // ~~~ 省略 ~~~
  }
}
```

# おわりに

`ProviderScope` の `override` を活用することで、
画面遷移時に引数を引き回すことなく、実装する方法について簡単にまとめました。
`Provider.family` の引数には、文字列以外にも独自に実装したクラスも渡せるので、
複数の引数を１つのクラスにまとめて設定することも可能です。

現在のアプリ開発準備室は、Flutterを活用した複数のアプリの企画開発と並行して、
実際にリリースしたモバイルアプリの運用フェーズに進みつつあります。
モバイルアプリ開発に限らず、企画で挙がった内容に併せて、Web版やAPIの開発も進めていきます。

明日は、 @ee4839 さんです。
よろしくお願いします。

ウェブクルーでは一緒に働いていただける方を随時募集しております。
お気軽にエントリーくださいませ。
https://www.webcrew.co.jp/recruit/