[読書会]Riverpod.dev/docs 翻訳及び補足(Migration guides From StateNotifier)

Riverpod.dev/docs(開始URL: https://riverpod.dev/docs/introduction/why_riverpod )には日本語訳ページ(/ja)も部分的には既にあるのですが、その部分はそのまま参照して、その他の英語訳のままのページについては自力で翻訳および解説を補足してみました。まだ工事中ですが最終的に満足ができたら上記サイトの翻訳ページに献上できないかな等という目標もあります。

本ブログの(上記Riverpod.dev/docsの)翻訳および解説の目次頁(構成上のトップページ)は次のURLになります。

本頁は、上記のうちの、「Migration guides / From StateNotifier」の章の翻訳および解説頁になります。

Migration guides

移行ガイド

From StateNotifier

Riverpod 2.0では、新しいクラス(Notifier / AsyncNotifer)が導入されました。
このページでは、非推奨のStateNotifierから新しいAPIに移行する方法を説明します。
AsyncNotifierによって導入された主な利点は、より優れた非同期サポートです。
実際、AsyncNotifierは、UIから変更する方法を公開できるFutureProviderと考えることができます。

さらに、新しい(Async)Notifierは、そのクラス内にRefオブジェクトを公開します。
コードジェン非コードジェンアプローチの間で同様の構文を提供します。
同期バージョンと非同期バージョンの間で同様の構文を提供します。
プロバイダからロジックを移動し、Notifier自体に集中させます。
Notifierの定義方法StateNotifierとの比較、非同期ステートのための新しいAsyncNotifier移行方法を見てみましょう。

さらに、新しい(Async)Notifiers

  1. Ref オブジェクトクラス内公開する。
  2. codegen アプローチと非 codegen アプローチの間で同様構文提供する。
  3. 同期バージョンと非同期バージョンの間で同様構文提供する。
  4. ロジックプロバイダから遠ざけNotifier 自体集中化します。

Notifier 定義する方法、StateNotifier との比較、および非同期状態用の新しい AsyncNotifier移行する方法を見てみましょう。

新しい構文の比較

この比較に入る前に、Notifier 定義する方法を必ず理解してください。
副作用の実行」を参照してください。

古い StateNotifier 構文を使用して例を書いてみましょう。

Dart
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() => state++;
  void decrement() => state--;
}

final counterNotifierProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

新しい Notifier API を使用して構築された同じ例を次に示します。大まかに訳すと次のようになります。

Dart
@riverpod
class CounterNotifier extends _$CounterNotifier {
  @override
  int build() => 0;

  void increment() => state++;
  void decrement() => state--;
}

Notifier と StateNotifier を比較すると、次の主な違いがわかります。

  1. StateNotifier のリアクティブな依存関係はプロバイダーで宣言されますが、Notifier はこのロジックをビルド メソッドで集中化します。
  2. StateNotifier の初期化プロセス全体はプロバイダーとコンストラクターに分割されますが、Notifier はそのようなロジックを配置する単一の場所を予約します。
  3. StateNotifier とは対照的に、Notifier のコンストラクターにはロジックがまったく書き込まれないことに注目してください。

Notifier の非同期同等物である AsyncNotifer でも同様の結論が得られます。

非同期 StateNotifier の移行

新しい API 構文の主な魅力は、非同期データの DX が改善されたことです。 次の例を見てみましょう。

Dart
class AsyncTodosNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
  AsyncTodosNotifier() : super(const AsyncLoading()) {
    _postInit();
  }

  Future<void> _postInit() async {
    state = await AsyncValue.guard(() async {
      final json = await http.get('api/todos');

      return [...json.map(Todo.fromJson)];
    });
  }

  // ...
}

新しい AsyncNotifier API で書き直された上記の例は次のとおりです。

Dart
@riverpod
class AsyncTodosNotifier extends _$AsyncTodosNotifier {
  @override
  FutureOr<List<Todo>> build() async {
    final json = await http.get('api/todos');

    return [...json.map(Todo.fromJson)];
  }

  // ...
}

AsyncNotifer は、Notifier と同様に、よりシンプルで統一された API を提供します。ここでは、AsyncNotifer がメソッドを備えた FutureProvider であることが簡単にわかります。

AsyncNotifer には、future update などのユーティリティとゲッターのセットが付属しています(StateNotifier にはありません!)。これにより、非同期の変更や副作用を処理するときに、はるかにシンプルなロジックを記述できます。副作用の実行も参照してください。

ヒント)
StateNotifier<AsyncValue<T>> から AsyncNotifer<T> への移行は、要約すると次のようになります。

  • 初期化ロジックをビルドに組み込む
  • 初期化または副作用メソッドでの catch/try ブロックの削除
  • Future を AsyncValue に変換するため、AsyncValue.guard をビルドから削除

利点

これらのいくつかの例の後、Notifier と AsyncNotifer の主な利点を強調しましょう。

  • 新しい構文は、特に非同期状態の場合、はるかにシンプルで読みやすくなるはずです。
  • 一般に、新しい API には定型コードが少なくなる可能性があります。
  • 記述しているプロバイダーの種類に関係なく、構文が統一され、コード生成が可能になりました。

さらに下に進み、さらに相違点と類似点を強調してみましょう。

.family および .autoDispose の明示的な変更

もう 1 つの重要な違いは、新しい API でファミリと自動破棄がどのように処理されるかです。

Notifier には、FamilyNotifier や AutoDisposeNotifier など、独自の .family および .autoDispose の対応物があります。
いつものように、このような変更は組み合わせることができます (別名 AutoDisposeFamilyNotifier)。

AsyncNotifer にも非同期の同等物があります (例: AutoDisposeFamilyAsyncNotifier)。
変更はクラス内で明示的に記述されます。すべてのパラメーターはビルド メソッドに直接挿入されるため、初期化ロジックで使用できます。これにより、読みやすさが向上し、簡潔になり、全体的に間違いが少なくなります。

StateNotifierProvider.family が定義されている次の例を見てみましょう。

Dart
class BugsEncounteredNotifier extends StateNotifier<AsyncValue<int>> {
  BugsEncounteredNotifier({
    required this.ref,
    required this.featureId,
  }) : super(const AsyncData(99));
  final String featureId;
  final Ref ref;

  Future<void> fix(int amount) async {
    state = await AsyncValue.guard(() async {
      final old = state.requireValue;
      final result = await ref.read(taskTrackerProvider).fix(id: featureId, fixed: amount);
      return max(old - result, 0);
    });
  }
}

final bugsEncounteredNotifierProvider =
    StateNotifierProvider.family.autoDispose<BugsEncounteredNotifier, int, String>((ref, id) {
  return BugsEncounteredNotifier(ref: ref, featureId: id);
});

BugsEncounteredNotifier は…重い/読みにくいと感じます。 移行された AsyncNotifier の対応物を見てみましょう。

Dart
@riverpod
class BugsEncounteredNotifier extends _$BugsEncounteredNotifier {
  @override
  FutureOr<int> build(String featureId) {
    return 99;
  }

  Future<void> fix(int amount) async {
    final old = await future;
    final result = await ref.read(taskTrackerProvider).fix(id: this.featureId, fixed: amount);
    state = AsyncData(max(old - result, 0));
  }
}

移行されたバージョンは軽い読み物のように感じられるはずです。

情報)
(Async)Notifier の .family パラメーターは、this.arg (または codegen を使用する場合は this.paramName) 経由で利用できます。

ライフサイクルの動作が異なる

Notifier/AsyncNotifier と StateNotifier の間のライフサイクルは大幅に異なります。

この例では、古い API がどのようにスパース ロジックを持っているかを再度示します。

Dart
class MyNotifier extends StateNotifier<int> {
  MyNotifier(this.ref, this.period) : super(0) {
    // 1 init logic
    _timer = Timer.periodic(period, (t) => update()); // 2 side effect on init
  }
  final Duration period;
  final Ref ref;
  late final Timer _timer;

  Future<void> update() async {
    await ref.read(repositoryProvider).update(state + 1); // 3 mutation
    if (mounted) state++; // 4 check for mounted props
  }

  @override
  void dispose() {
    _timer.cancel(); // 5 custom dispose logic
    super.dispose();
  }
}

final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
  // 6 provider definition
  final period = ref.watch(durationProvider); // 7 reactive dependency logic
  return MyNotifier(ref, period); // 8 pipe down `ref`
});

ここで、durationProvider が更新されると、MyNotifier は破棄されます。次に、そのインスタンスが再インスタンス化され、その内部状態が再初期化されます。
さらに、他のプロバイダとは異なり、dispose コールバックはクラス内で個別に定義されます。
最後に、プロバイダー内で ref.onDispose を記述することも可能であり、この API を使用するとロジックがいかにスパースになるかをもう一度示します。潜在的に、開発者はこの Notifier の動作を理解するために 8 か所 (8!) の異なる場所を調べる必要があるかもしれません。

これらの曖昧さは Riverpod 2.0 で解決されます。

Old dispose vs ref.onDispose

StateNotifier の destroy メソッドは、ノーティファイア自体の廃棄イベントを参照します。別名、それ自体を廃棄する前に呼び出されるコールバックです。

(非同期) 通知機能は再構築時に破棄されないため、このプロパティを持ちません。あるのは内部状態だけです。

新しい通知機能では、dispose ライフサイクルは、他のプロバイダーと同様に、ref.onDispose (およびその他) を介して 1 か所でのみ処理されます。これにより、API が簡素化され、DX も簡素化されるため、ライフサイクルの副作用を理解するために確認する場所は、build メソッドの 1 か所だけになります。

簡単に説明すると、内部状態が再構築される前に起動するコールバックを登録するには、他のプロバイダーと同様に ref.onDispose を使用できます。

上記のスニペットは次のように移行できます。

Dart
@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  int build() {
    // Just read/write the code here, in one place
    final period = ref.watch(durationProvider);
    final timer = Timer.periodic(period, (t) => update());
    ref.onDispose(timer.cancel);

    return 0;
  }

  Future<void> update() async {
    await ref.read(repositoryProvider).update(state + 1);
    // `mounted` is no more!
    state++; // This might throw.
  }
}

この最後のスニペットでは確かにいくらか単純化されていますが、未解決の問題がまだ残っています。更新の実行中に通知機能がまだ生きているかどうかを理解できません。 これにより、不要な StateError が発生する可能性があります。

No more mounted

これは、(Async)Notifiers に、StateNotifier で使用できたマウントされたプロパティがないために発生します。ライフサイクルの違いを考慮すると、これは完全に理にかなっています。マウントされたプロパティは可能ではありますが、新しい通知機能では誤解を招く可能性があります。マウントはほぼ常に true になります。

カスタムの回避策を作成することも可能ですが、非同期操作をキャンセルしてこれを回避することをお勧めします。

操作のキャンセルは、カスタムコンプリータまたは任意のカスタム派生物を使用して実行できます。

たとえば、Dio を使用してネットワーク リクエストを実行している場合は、キャンセル トークンの使用を検討してください (「キャッシュのクリアと状態破棄への対応」も参照)。

したがって、上記の例は次のように移行します。

Dart
@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  int build() {
    // Just read/write the code here, in one place
    final period = ref.watch(durationProvider);
    final timer = Timer.periodic(period, (t) => update());
    ref.onDispose(timer.cancel);

    return 0;
  }

  Future<void> update() async {
    final cancelToken = CancelToken();
    ref.onDispose(cancelToken.cancel);
    await ref.read(repositoryProvider).update(state + 1, token: cancelToken);
    // When `cancelToken.cancel` is invoked, a custom Exception is thrown
    state++;
  }
}

ミューテーション API は以前と同じです。

これまで、StateNotifier と新しい API の違いを示してきました。 代わりに、Notifier、AsyncNotifer、StateNotifier に共通するのは、その状態がどのように消費され、変更されるかということです。

Consumersは、同じ構文を使用してこれら 3 つのプロバイダからデータを取得できます。これは、StateNotifier から移行する場合に最適です。これは通知メソッドにも当てはまります。

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterNotifierProvider);
    return Column(
      children: [
        Text("You've counted up until $counter, good job!"),
        TextButton(
          onPressed: ref.read(counterNotifierProvider.notifier).increment,
          child: const Text('Count even more!'),
        )
      ],
    );
  }
}

その他の移行

StateNotifier と Notifier (または AsyncNotifier) の影響の少ない違いを調べてみましょう。

.addListener および .stream から

StateNotifier の .addListener および .stream を使用して、状態の変更をリッスンできます。これら 2 つの API は現在では時代遅れとみなされています。
これは、Notifier、AsyncNotifier、およびその他のプロバイダーとの API の完全な統一性を達成するための意図的なものです。 確かに、Notifier または AsyncNotifier の使用は、他のプロバイダーと何ら変わらないはずです。

(移行前)
class MyNotifier extends StateNotifier<int> {
  MyNotifier() : super(0);

  void add() => state++;
}

final myNotifierProvider = StateNotifierProvider<MyNotifier, int>((ref) {
  final notifier = MyNotifier();

  final cleanup = notifier.addListener((state) => debugPrint('$state'));
  ref.onDispose(cleanup);

  // Or, equivalently:
  // final listener = notifier.stream.listen((event) => debugPrint('$event'));
  // ref.onDispose(listener.cancel);

  return notifier;
});
(移行後)
@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  int build() {
    ref.listenSelf((_, next) => debugPrint('$next'));
    return 0;
  }

  void add() => state++;
}

一言で言えば、Notifier/AsyncNotifer をリッスンしたい場合は、ref.listen を使用するだけです。 「リクエストの結合」を参照してください。

テストの .debugState から

StateNotifier は .debugState を公開します。このプロパティは、pkg:state_notifier ユーザーが開発モードのときにテスト目的でクラスの外部から状態にアクセスできるようにするために使用されます。

テストで状態にアクセスするために .debugState を使用している場合は、このアプローチを削除する必要がある可能性があります。

Notifier / AsyncNotifer には .debugState がありません。代わりに、.state (@visibleForTesting) を直接公開します。

危険)
テストから .state にアクセスすることは避けてください。必要な場合は、Notifier / AsyncNotifer がすでに適切にインスタンス化されている場合にのみ実行してください。そうすれば、テスト内の .state に自由にアクセスできるようになります。

確かに、Notifier / AsyncNotifier は手動でインスタンス化すべきではありません。代わりに、プロバイダーを使用して対話する必要があります。そうしないと、ref および family 引数が初期化されていないため、notifierが壊れます。

Notifier インスタンスをお持ちでない場合は、 問題ありません。公開された状態を読み取るのと同じように、ref.read を使用して取得できます。

Dart
void main(List<String> args) {
  test('my test', () {
    final container = ProviderContainer();
    addTearDown(container.dispose);

    // Obtaining a notifier
    final AutoDisposeNotifier<int> notifier = container.read(myNotifierProvider.notifier);

    // Obtaining its exposed state
    final int state = container.read(myNotifierProvider);

    // TODO write your tests
  });
}

テストの詳細については、専用ガイドをご覧ください。 「プロバイダーのテスト」を参照してください。

StateProviderから

StateProvider はリリース以来 Riverpod によって公開されており、StateNotifierProvider の簡易バージョン用にいくつかの LoC を保存するために作成されました。
StateNotifierProvider は非推奨であるため、StateProvider も使用しないでください。 さらに、現時点では、新しい API に相当する StateProvider はありません。

それにもかかわらず、StateProvider から Notifier への移行は簡単です。

(移行前)
final counterProvider = StateProvider<int>((ref) {
  return 0;
});
(移行後)
@riverpod
class CounterNotifier extends _$CounterNotifier {
  @override
  int build() => 0;

  @override
  set state(int newState) => super.state = newState;
  int update(int Function(int state) cb) => state = cb(state);
}

LoC(コード行数) がさらにいくつかかかりますが、StateProvider から移行することで、StateNotifier を明確にアーカイブできるようになります。

補足)
StateProvider背景
 Riverpodの初期リリース時、StateProvider簡易的なStateNotifierProviderの代替として提供されていました。StateProviderを使うことで、コード行数(LoC)を減らし、簡略化できる利点がありました。

StateProvider非推奨
 現在、StateNotifierProviderが非推奨になったため、それに伴いStateProviderも推奨されなくなりました

Notifier APIへの移行
 文献では、StateProviderからNotifierへの移行容易であることが説明されていますが、Notifierに移行すると、若干コード行数が増える(「a few more LoC」)という指摘があります。

 この文献では、LoC(コード行数)が少し増えることを許容しつつ、新しいNotifier API移行することの重要性が強調されています。コード行数を抑えることも開発では重要ですが、より長期的なメンテナンス拡張性を考慮すると、Notifier使用推奨されるということです。

(次の記事はこちら)

コメントを残す