YUMEMI Flutter Advent Calendar 2023 21日目の記事です。
はじめに
はじめまして、ノリでアドカレ5本書くことにしたもののネタが無いK9iです。
今回はRiverpodを使う際にdependenciesに追加しているものの、なんなのかよく分かってない人もいるかもしれないriverpod_annotationについて記事にしてみました。
本編
riverpod_annotation
riverpod_annotationは名前の通りriverpod_generatorと組み合わせて使うannotation(注釈)のパッケージです。
使い方
riverpod_generatorを使うときに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とは
コードにメタデータを追加する構文です。
具体例
riverpod_annotationには記事執筆時点で3つのannotationがあります。
順番に見ていきます。
ソースはここにあります。
Riverpodアノテーション
riverpod_generatorでProviderの元となる関数、もしくはクラスに@riverpodのように指定するアレです。
keepAliveもしくはdependenciesを引数に取ります。引数があるときは大文字始まりで@Riverpod、無いときは@riverpodとします。@riverpodは実装的にはconst riverpod = Riverpod();となっていることが分かります👀
@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があるのでこれの参照をたどっていけば良さそうです。
/// Matches with the `Riverpod` annotation from riverpod_annotation.
const riverpodType =
TypeChecker.fromName('Riverpod', packageName: 'riverpod_annotation');
RiverpodAnnotationというクラスがあり、そこでriverpodTypeが使われています。
抽象構文木 (Abstract Syntax Tree: AST)をRiverpodアノテーションにパースしており、riverpodTypeかで判定しています。
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が継承しています。
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していることが分かります。
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で使うなと書いてあり、ユーザー向けではないようです👀
/// An annotation used to help the linter find the user-defined element from
/// the generated provider.
///
/// DO NOT USE
class ProviderFor {
ではどこで使われているかというと.g.dartで使われています。
以下のような生成元関数があるとき
@riverpod
String hoge(HogeRef ref) {
return 'hoge';
}
生成されたProviderに@ProviderFor(hoge)のように付与されます。
詳細は省きますが、ASTを解析するときにriverpod_generatorを使ってないProviderを見分けるのにこれが使われていました。(ProviderForアノテーションが付いてるならriverpod_generatorが生成したProviderと判定)
/// 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種類の使い方が説明されています。
/// {@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の有無が違う関数を用意すると
@riverpod
Future<String> hoge(HogeRef ref) async {
return 'hoge';
}
@riverpod
Raw<Future<String>> fuga(FugaRef ref) async {
return 'fuga';
}
生成コードはAutoDisposeFutureProviderとAutoDisposeProviderで確かに違いました!
// 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を使うことで解決できます。
@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アノテーションについて説明しました🙌