[読書会]Riverpod.dev/docs 翻訳及び補足(About code generation)

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

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

本頁は、上記のうちの、「About code generation」の章の翻訳および解説頁になります。

About code generation

注釈)現在Riverpod.dev/docs中の本章「About code generation」には既に日本語訳ページ(https://riverpod.dev/ja/docs/concepts/about_code_generation)が存在しますので、原文はこちらを参照願います。当ブログではこの日本語訳はそのまま参照して補足を追加しています。

コード生成について

コード生成、ツールを使用してコードを自動生成するアイデアです。
Dart では、アプリケーションを”コンパイル”する際に追加のステップが必要です。
この問題は、近い将来、Dart チームがこの問題に対処しようとしているため、解決される可能性があります。

Riverpod では、コード生成は”provider”を定義するための構文を変更することを意味します。
以下が例です:

(もしコード自動生成を使わなかった場合)
final fetchUserProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
  final json = await http.get('api/user/$userId');
  return User.fromJson(json);
});

※以下を手動で忘れずに記述する必要がある:
 (1)FutureProviderを使ってプロバイダ作成コードが必要
 (2).autoDispose(不要時の廃棄)
 (3).family(パラメータ追加分)

コード生成を使用すると、次のように書くことができます:

(コード自動生成を使う場合)
@riverpod
Future<User> fetchUser(FetchUserRef ref, {required int userId}) async {
  final json = await http.get('api/user/$userId');
  return User.fromJson(json);
}

※上記(1)~(3)を、@riverpod が自動生成してくれる。
 (ただし、@riverpod が自動生成するための内容は明示する必要はある。
  (1)→ fetchUser を命名したなら、FetchUserRef ref を書く。
       引数 int userId も書く。
  (2)→ 不要
  (3)→ 不要

Riverpod を使う時、コード生成は完全にオプションです。
Riverpod を使用するために必須ではありません。
ただし、Riverpod はコード生成をサポートしており、使用を推奨しています。

Riverpod のコード生成のインストール方法と使用方法については、 本サイト冒頭を参照してください。
ドキュメントのサイドバーでコード生成を有効にすることを忘れないでください。

コード生成を使用すべきか?

Riverpod ではコード生成はオプションです。
その点を考慮すると、使用すべきかどうか疑問に思うかもしれません。

答えは:使用した方が良いです。
コード生成を使うことは Riverpod を使用する上で推奨される方法です。
これは将来性があり、Riverpod を最大限に活用することができます。
多くのアプリケーションがすでにFreezed json_serializableのパッケージでコード生成を使用しています。
その場合、プロジェクトはすでにコード生成の設定がされており、Riverpod を使用することは簡単です。

現在、コード生成がオプションなのはbuild_runnerが多くの人に好まれないためです。
しかし、Static Metaprogrammingが Dart で利用可能になると、 build_runnerはもはや問題ではありません。
その時点で、コード生成を使用することが Riverpod で唯一の方法になるでしょう。

build_runnerを使用することが大きな問題であれば、その場合のみコード生成を使用しないことを検討してください。 ただし、その場合、一部の機能が使用できなくなり、将来的にコード生成に移行する必要があります。
その際、Riverpod はマイグレーションをスムーズに進めるためのツールを提供します。

コード生成を使用する利点は何?

“Riverpod でコード生成がオプションなら、なぜ使用するのか?”と思うかもしれません。

いつものように、パッケージを使う理由は:開発を楽にするためです。
これには以下が含まれますが、限定されるものではありません:

より良い構文、より読みやすく柔軟性があり、学習曲線が低い。
provider のタイプを気にする必要はありません。
ロジックを書き、Riverpod が最も適した provider を選択します。
構文は”よくないグローバル変数”を定義しているようには見えません。代わりにカスタム関数/クラスを定義します。
provider にパラメータを渡すことが制限されなくなりました。.familyを使用して単一の位置パラメータを渡すことを制限される代わりに、任意のパラメータを渡すことができます。 これには名前付きパラメータ、オプション、さらにはデフォルト値も含まれます。
ステートフルなホットリロード が Riverpod で書かれたコードに適用されます。
追加のメタデータを生成し、それをデバッガーが拾い上げることで、より良いデバッグが可能になります。
いくつかの Riverpod の機能はコード生成でのみ利用可能になります。

構文

provider の定義

コード生成を使って provider を定義する時、以下のようなことに注意してください。

  • provider は注釈付きの関数または注釈付きのクラスとして定義できます。 どちらもほぼ同じですが、クラスベースの provider は(公開)メソッドを含む利点があり、外部のオブジェクトが provider の状態を変更することができます(副作用)。 関数ベースの provider は、build メソッドだけを持つクラスベースの provider を記述するためのシンタックスシュガーであり、UI によって変更することはできません。
  • 全ての Dart のasyncプリミティブ(Future、FutureOr、および Stream)がサポートされています。
  • 関数が asyncとしてマークされると、プロバイダーは自動的にエラー/読み込み状態を処理し、AsyncValue を公開します。

Functional(関数)と、Class(クラス)どちらで定義するかによる比較:

Functional(関数)                      

メソッドを持たない
buildメソッドが無い
 ⇒(副作用は無い)□UI変更は無い

※厳密にはオーバーライドすれば、
 メソッドやbuildメソッドを追加できるが、
 それはRiverpodの思想に反しているので、
 あえてこの様に区別して説明されている。

Classクラス
利点
(公開)メソッドを持たせることができる
buildメソッドを持つ
 ⇒(副作用)■外部からプロバイダの状態を変更できる

※「公開」というキーワードは、ここでは無視した方が
 分かり易いかもしれません。
 Remiさんは、メソッド追加によるメリット、
 buildメソッドによるメリットを強調したい、
 という事だと思います。

上記の、Sync(同期)、Async – Future(非同期 – 遅延)、Async – Stream(非同期、ストリーム)での実装例:

Sync(関数)
@riverpod
String example(ExampleRef ref) {
  return 'foo';
}

※厳密にはオーバーライドすれば、
メソッドやbuildメソッドを追加できるが、
それはRiverpodの思想に反している。
※シンプルに状態管理するだけ。
Sync(クラス)
@riverpod
class Example extends _$Example {
  @override
  String build() {
    return 'foo';
  }

  //ここに状態変更用メソッドを追加可能
}
Async – Future(関数)
@riverpod
Future<String> example(ExampleRef ref) async {
  return Future.value('foo');
}
※厳密にはオーバーライドすれば、
メソッドやbuildメソッドを追加できるが、
それはRiverpodの思想に反している。
※シンプルに状態管理するだけ。
Async – Future(クラス)
@riverpod
class Example extends _$Example {
  @override
  Future<String> build() async {
    return Future.value('foo');
  }

  //ここに状態変更用メソッドを追加可能
}
Async – Stream(関数)
@riverpod
Stream<String> example(ExampleRef ref) async* {
  yield 'foo';
}
※厳密にはオーバーライドすれば、
メソッドやbuildメソッドを追加できるが、
それはRiverpodの思想に反している。
※シンプルに状態管理するだけ。
Async – Stream(クラス)
@riverpod
class Example extends _$Example {
  @override
  Stream<String> build() async* {
    yield 'foo';
  }

  //ここに状態変更用メソッドを追加可能
}

autoDispose の有効/無効化

コード生成を使う時、プロバイダデフォルトで自動破棄されます
これは、リスナーがアタッチされていないときに自動的に破棄されることを意味します(ref.watch/ref.listen)。 このデフォルト設定は、Riverpod の理念によりよく一致しています。
非コード生成バリアントでは、package:providerから移行するユーザーに対応するため、autoDispose はデフォルトで無効になっていました。

自動破棄を無効にしたい場合は、keepAlive: trueアノテーションを記載することで可能です。

Dart
// AutoDispose provider (keepAlive is false by default)
@riverpod
String example1(Example1Ref ref) => 'foo';

// Non autoDispose provider
@Riverpod(keepAlive: true)
String example2(Example2Ref ref) => 'foo';

provider にパラメータを渡す (family)

コード生成を使用する場合、provider にパラメータを渡すためには、.family修飾子を使用する必要はありません
代わりに、provider のメイン関数は、名前付きパラメータ、オプションのパラメータ、デフォルト値を含む任意の数のパラメータを受け取ることができます。
ただし、これらのパラメータは依然として一貫性のある==を持つ必要があることに注意してください。
つまり、値はキャッシュされるか、パラメータは==をオーバーライドする必要があります。

(Functional(関数))
@riverpod
String example(
  ExampleRef ref,
  int param1, {
  String param2 = 'foo',
}) {
  return 'Hello $param1 & param2';
}




(Class-Based(クラス))
@riverpod
class Example extends _$Example {
  @override
  String build(
    int param1, {
    String param2 = 'foo',
  }) {
    return 'Hello $param1 & param2';
  }

  //ここに状態変更用メソッドを追加可能

}

非コード生成バリアントからの移行

非コード生成バリアントを使用する場合、provider の型を直接指定する必要があります。
以下は、コード生成に移行するための対応オプションです。

Provider:

(Before(移行前))非自動生成用コード:
final exampleProvider = Provider.autoDispose<String>(
  (ref) {
    return 'foo';
  },
);
(After(移行後))自動生成用コード:
@riverpod
String example(ExampleRef ref) {
  return 'foo';
}

NotifierProvider:

(Before(移行前))非自動生成用コード:
final exampleProvider = NotifierProvider.autoDispose<ExampleNotifier, String>(
  ExampleNotifier.new,
);

class ExampleNotifier extends AutoDisposeNotifier<String> {
  @override
  String build() {
    return 'foo';
  }

  // Add methods to mutate the state
}
(After(移行後))自動生成用コード:
@riverpod
class Example extends _$Example {
  @override
  String build() {
    return 'foo';
  }

  // Add methods to mutate the state
}

FutureProvider:

(Before(移行前))非自動生成用コード:
final exampleProvider =
    FutureProvider.autoDispose<String>((ref) async {
  return Future.value('foo');
});
(After(移行後))自動生成用コード:
@riverpod
Future<String> example(ExampleRef ref) async {
  return Future.value('foo');
}

FutureProvider:

(Before(移行前))非自動生成用コード:
final exampleProvider =
    StreamProvider.autoDispose<String>((ref) async* {
  yield 'foo';
});
(After(移行後))自動生成用コード:
@riverpod
Stream<String> example(ExampleRef ref) async* {
  yield 'foo';
}

AsyncNotifierProvider:

(Before(移行前))非自動生成用コード:
final exampleProvider =
    AsyncNotifierProvider.autoDispose<ExampleNotifier, String>(
  ExampleNotifier.new,
);

class ExampleNotifier extends AutoDisposeAsyncNotifier<String> {
  @override
  Future<String> build() async {
    return Future.value('foo');
  }

  // Add methods to mutate the state
}
(After(移行後))自動生成用コード:
@riverpod
class Example extends _$Example {
  @override
  Future<String> build() async {
    return Future.value('foo');
  }

  // Add methods to mutate the state
}

StreamNotifierProvider:

(Before(移行前))非自動生成用コード:
final exampleProvider =
    StreamNotifierProvider.autoDispose<ExampleNotifier, String>(() {
  return ExampleNotifier();
});

class ExampleNotifier extends AutoDisposeStreamNotifier<String> {
  @override
  Stream<String> build() async* {
    yield 'foo';
  }

  // Add methods to mutate the state
}
(After(移行後))自動生成用コード:
@riverpod
class Example extends _$Example {
  @override
  Stream<String> build() async* {
    yield 'foo';
  }

  // Add methods to mutate the state
}

(次の記事はこちら)

コメントを残す