10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

YUMEMI FlutterAdvent Calendar 2023

Day 21

riverpod_annotationについてちょっと詳しくなるための記事

Last updated at Posted at 2023-12-20

YUMEMI Flutter Advent Calendar 2023 21日目の記事です。

はじめに

はじめまして、ノリでアドカレ5本書くことにしたもののネタが無いK9iです。
今回はRiverpodを使う際にdependenciesに追加しているものの、なんなのかよく分かってない人もいるかもしれないriverpod_annotationについて記事にしてみました。

本編

riverpod_annotation

riverpod_annotationは名前の通りriverpod_generatorと組み合わせて使うannotation(注釈)のパッケージです。

使い方

riverpod_generatorを使うときにpubspec.yamlに記述します。

pubspec.yaml
dependencies:
    flutter_riverpod:
  riverpod_annotation:

dev_dependencies:
  build_runner:
  riverpod_generator:

riverpod_generatorがdev_dependenciesなのに対して、riverpod_annotationはdependenciesに記述します。
同様の例としてfreezedとfreezed_annotation、json_serializableとjson_annotationがありますね。

annotationとは

ChatGPTの回答
image.png

コードにメタデータを追加する構文です。

具体例

riverpod_annotationには記事執筆時点で3つのannotationがあります。
順番に見ていきます。
ソースはここにあります。

Riverpodアノテーション

riverpod_generatorでProviderの元となる関数、もしくはクラスに@riverpodのように指定するアレです。
keepAliveもしくはdependenciesを引数に取ります。引数があるときは大文字始まりで@Riverpod、無いときは@riverpodとします。@riverpodは実装的にはconst riverpod = Riverpod();となっていることが分かります👀

riverpod_annotation.dart
@Target({TargetKind.classType, TargetKind.function})
@sealed
class Riverpod {
  const Riverpod({
    this.keepAlive = false,
    this.dependencies,
  });

~~~

@Target({TargetKind.classType, TargetKind.function})
const riverpod = Riverpod();
Riverpodアノテーションの利用箇所

せっかくなのでRiverpodのコード上でどのようにRiverpodアノテーションが使われいるか軽く見てみます。
riverpod_analyzer_utilsというRiverpod内部で使われてるパッケージのriverpod_types.dartにTypeChekerがあるのでこれの参照をたどっていけば良さそうです。

riverpod_types.dart
/// Matches with the `Riverpod` annotation from riverpod_annotation.
const riverpodType =
    TypeChecker.fromName('Riverpod', packageName: 'riverpod_annotation');

RiverpodAnnotationというクラスがあり、そこでriverpodTypeが使われています。
抽象構文木 (Abstract Syntax Tree: AST)をRiverpodアノテーションにパースしており、riverpodTypeかで判定しています。

riverpod_annotation.dart
class RiverpodAnnotation extends RiverpodAst {
  RiverpodAnnotation._({
    required this.annotation,
    required this.element,
    required this.keepAliveNode,
    required this.dependencies,
  });

  static RiverpodAnnotation? _parse(
    Declaration node,
  ) {
    final annotatedElement = node.declaredElement;
    if (annotatedElement == null) return null;

    for (final annotation in node.metadata) {
      final elementAnnotation = annotation.elementAnnotation;
      final annotationElement = annotation.element;
      if (elementAnnotation == null || annotationElement == null) continue;
      if (annotationElement is! ExecutableElement ||
          !riverpodType.isExactlyType(annotationElement.returnType)) {
        // The annotation is not an @Riverpod
        continue;
      }

RiverpodAnnotationはGeneratorProviderDeclarationのgetterの返り値に使われています。GeneratorProviderDeclarationはriverpod_generatorで生成されるProviderの元となるコードの宣言のようです。
GeneratorProviderDeclarationはabstract classになっており、ClassBasedProviderDeclarationとFunctionalProviderDeclarationが継承しています。

generator_provider_declaration.dart
abstract class GeneratorProviderDeclaration extends ProviderDeclaration {
  @override
  GeneratorProviderDeclarationElement get providerElement;
  RiverpodAnnotation get annotation;

ClassBasedProviderDeclarationとFunctionalProviderDeclarationもstaticな生成用メソッドがあり、ClassDeclarationかFunctionDeclarationをRiverpodAnnotation._parseに渡しています。パースに失敗してnullのときは早期returnしています。
これによりRiverpodアノテーションがついた生成元コードかを判定しています。

class ClassBasedProviderDeclaration extends GeneratorProviderDeclaration {
~~~

  static ClassBasedProviderDeclaration? _parse(
    ClassDeclaration node,
    _ParseRefInvocationMixin parent,
  ) {
    final element = node.declaredElement;
    if (element == null) return null;
    final riverpodAnnotation = RiverpodAnnotation._parse(node);
    if (riverpodAnnotation == null) return null;
~~~
class FunctionalProviderDeclaration extends GeneratorProviderDeclaration {
~~~

  static FunctionalProviderDeclaration? _parse(
    FunctionDeclaration node,
    _ParseRefInvocationMixin parent,
  ) {
    final element = node.declaredElement;
    if (element == null) return null;
    final riverpodAnnotation = RiverpodAnnotation._parse(node);
    if (riverpodAnnotation == null) return null;

Riverpodアノテーションによってriverpod_generator用の生成元コードかを判定する仕組みが分かりました🥳

おまけ Riverpodアノテーションその他の利用箇所

riverpodTypeはriverpod_generatorパッケージ以外にも、riverpod_lintパッケージからも使われています。
例えば以下はRiverpodアノテーションがついたクラスにbuild methodが無いとき警告するlintルールです。registryに登録する際Riverpodアノテーションが無いときはreturnしていることが分かります。

notifier_build.dart
class NotifierBuild extends RiverpodLintRule {
  const NotifierBuild() : super(code: _code);

  static const _code = LintCode(
    name: 'notifier_build',
    problemMessage:
        'Classes annotated by `@riverpod` must have the `build` method',
  );

  @override
  void run(
    CustomLintResolver resolver,
    ErrorReporter reporter,
    CustomLintContext context,
  ) {
    context.registry.addClassDeclaration((node) {
      final hasRiverpodAnnotation = node.metadata.where(
        (element) {
          final annotationElement = element.element;

          if (annotationElement == null ||
              annotationElement is! ExecutableElement) return false;

          return riverpodType.isExactlyType(annotationElement.returnType);
        },
      ).isNotEmpty;

      if (!hasRiverpodAnnotation) return;

ProviderForアノテーション

RiverpodForというアノテーションがあります。こちらはdartdocで使うなと書いてあり、ユーザー向けではないようです👀

riverpod_annotation.dart
/// An annotation used to help the linter find the user-defined element from
/// the generated provider.
///
/// DO NOT USE
class ProviderFor {

ではどこで使われているかというと.g.dartで使われています。
以下のような生成元関数があるとき

hoge.dart
@riverpod
String hoge(HogeRef ref) {
  return 'hoge';
}

生成されたProviderに@ProviderFor(hoge)のように付与されます。
詳細は省きますが、ASTを解析するときにriverpod_generatorを使ってないProviderを見分けるのにこれが使われていました。(ProviderForアノテーションが付いてるならriverpod_generatorが生成したProviderと判定)

hoge.g.dart
/// See also [hoge].
@ProviderFor(hoge)
final hogeProvider = AutoDisposeProvider<String>.internal(
  hoge,
  name: r'hogeProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : _$hogeHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef HogeRef = AutoDisposeProviderRef<String>;

Rawアノテーション

Rawが最後のアノテーションです。docに2種類の使い方が説明されています。

riverpod_annotation.dart
/// {@template riverpod_annotation.raw}
/// An annotation for marking a value type as "should not be handled
/// by Riverpod".
///
/// This is a type-alias to [T], and has no runtime effect. It is only used
/// as metadata for the code-generator/linter.
///
/// This serves two purposes:
/// - It enables a provider to return a [Future]/[Stream] without
///   having the provider converting it into an [AsyncValue].
///   ```dart
///   @riverpod
///   Raw<Future<int>> myProvider(...) async => ...;
///   ...
///   // returns a Future<int> instead of AsyncValue<int>
///   Future<int> value = ref.watch(myProvider);
///   ```
///
/// - It can silence the linter when a provider returns a value that
///   is otherwise not supported, such as Flutter's `ChangeNotifier`:
///   ```dart
///   // Will not trigger the "unsupported return type" lint
///   @riverpod
///   Raw<MyChangeNotifier> myProvider(...) => MyChangeNotifier();
///   ```
///
/// The typedef can be used at various places within the return type declaration.
///
/// For example, a valid return type is `Future<Raw<ChangeNotifier>>`.
/// This way, Riverpod will convert the [Future] into an [AsyncValue], and
/// the usage of `ChangeNotifier` will not trigger the linter:
///
/// ```dart
/// @riverpod
/// Future<Raw<ChangeNotifier>> myProvider(...) async => ...;
/// ...
/// AsyncValue<ChangeNotifier> value = ref.watch(myProvider);
/// ```
///
/// {@endtemplate}
typedef Raw<T> = T;
Rawの使い方1

FutureやStreamをAsyncValueを使わず返す。
試してみましょう。
以下のようなにRawの有無が違う関数を用意すると

hoge.dart
@riverpod
Future<String> hoge(HogeRef ref) async {
  return 'hoge';
}

@riverpod
Raw<Future<String>> fuga(FugaRef ref) async {
  return 'fuga';
}

生成コードはAutoDisposeFutureProviderとAutoDisposeProviderで確かに違いました!

hoge.g.dart
// See also [hoge].
@ProviderFor(hoge)
final hogeProvider = AutoDisposeFutureProvider<String>.internal(
  hoge,
  name: r'hogeProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : _$hogeHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef HogeRef = AutoDisposeFutureProviderRef<String>;
String _$fugaHash() => r'18dcf3ebf332c9ef412a0feb131a9b372c683131';

/// See also [fuga].
@ProviderFor(fuga)
final fugaProvider = AutoDisposeProvider<Future<String>>.internal(
  fuga,
  name: r'fugaProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : _$fugaHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef FugaRef = AutoDisposeProviderRef<Future<String>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
Rawの使い方2

riverpod_generatorはChangeNotifeirを継承したクラスなどをサポートしていません。
以下のriverpod_lintのルールで警告されます。

GoRouterなどもChangeNotifier継承しているためこのルールに引っかかります。
この際Rawを使うことで解決できます。

READMEの例
@riverpod
Raw<GoRouter> myRouter(MyRouterRef ref) {
  final router = GoRouter(...);
  // Riverpod won't dispose the ChangeNotifier for you in this case. Don't forget
  // to do it on your own!
  ref.onDispose(router.dispose);
  return router;
}

まとめ

riverpod_annotationのRiverpod、ProviderFor、Rawアノテーションについて説明しました🙌

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?