【Flutter】Riverpod Generator基本

対象者

  • FlutterとRIverpodを使った開発経験があり、より効率的な状態管理の方法を探している方
  • Riverpod Generatorについて具体的な知識を得たい方
  • 最新の技術トレンドに興味があり、自己のスキルセットを常にアップデートしたい方

はじめに

Flutterの状態管理パッケージで有名なのが Riverpod です。Riverpod についての詳細は以下を参照ください。

【Flutter】Riverpod 2.1系 完全攻略

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を使ったコード生成をまとめました。あまり良いサンプルがなかったので、自分用のメモでもあります。

参考

全ソース

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),
      ),
    );
  }
}