対象者
- FlutterとRIverpodを使った開発経験があり、より効率的な状態管理の方法を探している方
- Riverpod Generatorについて具体的な知識を得たい方
- 最新の技術トレンドに興味があり、自己のスキルセットを常にアップデートしたい方
はじめに
Flutterの状態管理パッケージで有名なのが Riverpod です。Riverpod についての詳細は以下を参照ください。
Riverpod Generatorとは、Riverpod用の便利なコード生成ツールです。開発者は自動的にRiverpodのプロバイダーを作成することができます。
Riverpod Generator
RiverpodのProviderの定義を書きやすくするために、Riverpod Generatorというコード自動生成の仕組みがあります。こちらを使えば、Providerの定義が簡単になります。
@riverpodというアノテーションを使って関数風に定義し、build_runnerを使って、対応するRiverpod用のクラスを自動生成します。
インストール
flutter pub add flutter_riverpod
flutter pub add riverpod_annotation
flutter pub add build_runner --dev
flutter pub add riverpod_generator --dev
flutter pub get
StateNotifierの代わり
いままで以下のように記述してきました。
Riverpod Generator 未使用:
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void countUp() {
state++;
}
}
final _counterProvider =
StateNotifierProvider<CounterNotifier, int>((ref) => CounterNotifier());
それが、以下のように簡潔に書けるようになりました。
「extends」の後は「_$(クラス名)」で、固定です。この名前で、クラスができます。
Riverpod Generator 使用:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';
@riverpod
class Count extends _$Count {
@override
int build() => 0;
void increase() => state++;
}
上記のコードを書いてから、以下のターミナルでコマンドを実行します。
flutter pub run build_runner build --delete-conflicting-outputs
そうすると、countProviderというProviderと、CountRefというクラスが自動生成されます。
build内では、以下のようにして値を使用します。
Text(ref.watch(countProvider).toString())
Providerの代わり
ほぼ定数のため、Riverpod使う意味あるの?という気もしないですが、以下のように書けるようになりました。
@Riverpod(keepAlive: true)
String title(TitleRef ref) => 'Riverpod Generator Sample';
アノテーションには、@riverpodと@Riverpodがあります。
@riverpodにはautoDispose がつき、参照されなくなったらメモリから削除されます。
@RiverpodはkeepAliveという引数があります。
- falseであれば上記と同様autoDisposeがつき、参照されなくなったらメモリから削除されます
- trueであれば、autoDisposeがつかず、参照されなくなってもメモリから削除されない
FutureProviderの代わり
いままでFutureProviderとして書いていたProviderが以下のように定義できるようになった。FutureProviderでは引数が1つしか取れなかった。しかしRiverpod Generatatorを使えば、いくらでも引数を増やせる!
@riverpod
Future<int> futureCount(FutureCountRef ref, {required int counter}) async {
await Future.delayed(Duration(seconds: 1));
Future.delayed(Duration(seconds: 1)).then((_) => _stream.sink.add(counter));
return counter;
}
上記の記述と自動生成を実施することによって、futureCountProviderが生成されます。
表示の仕方自体は通常のRiverpodと変わりません。エラー時と読込中、データ取得時のそれぞれの3つの状態のWidgetを定義します。
ref
.watch(futureCountProvider(counter: ref.watch(countProvider)))
.when(
error: (error, st) => Text(error.toString()),
loading: () => const CircularProgressIndicator(),
data: (data) => Text(data.toString()),
),
StreamProviderの代わり
Futureのケースと同様に、複数の引数を撮ることができるようになりました!(例では引数使ってませんけど)
- 定義
final _stream = StreamController<int>();
@riverpod
Stream<int> streamCount(StreamCountRef ref) {
return _stream.stream;
}
- 表示
ref.watch(streamCountProvider).when(
error: (error, st) => Text(error.toString()),
loading: () => const CircularProgressIndicator(),
data: (data) => Text(data.toString()),
),
StateProviderの代わり
ない。対応する気もない(ソース)
つぶやき
いつのまにかプライベートクラスにできるようになってる。FlutterGenerator登場時は、「_Count」ではエラーになったが、いまでは使える。グローバル変数が嫌いなので、プライベートクラスなproviderにして処理したい、、
まとめ
ということでRiverpod Generatorを使ったコード生成をまとめました。あまり良いサンプルがなかったので、自分用のメモでもあります。
参考
- Riverpod本家
- riverpod_generator 公式ページ
- How to Auto-Generate your Providers with Flutter Riverpod Generator
- 【Flutter Riverpod:第18回】Riverpod generatorの説明とインストール
全ソース
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';
void main() {
runApp(ProviderScope(child: MyApp()));
}
@Riverpod(keepAlive: true)
String title(TitleRef ref) => 'Riverpod Generator Sample';
@riverpod
class Count extends _$Count {
@override
int build() => 0;
void increase() => state++;
}
@riverpod
Future<int> futureCount(FutureCountRef ref, {required int counter}) async {
await Future.delayed(Duration(seconds: 1));
Future.delayed(Duration(seconds: 1)).then((_) => _stream.sink.add(counter));
return counter;
}
final _stream = StreamController<int>();
@riverpod
Stream<int> streamCount(StreamCountRef ref) {
return _stream.stream;
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends ConsumerWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text(ref.watch(titleProvider)),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${ref.watch(countProvider)}',
style: Theme.of(context).textTheme.headline4,
),
ref
.watch(futureCountProvider(counter: ref.watch(countProvider)))
.when(
error: (error, st) => Text(error.toString()),
loading: () => const CircularProgressIndicator(),
data: (data) => Text(data.toString()),
),
ref.watch(streamCountProvider).when(
error: (error, st) => Text(error.toString()),
loading: () => const CircularProgressIndicator(),
data: (data) => Text(data.toString()),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: ref.read(countProvider.notifier).increase,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}