😸

flutter_riverpodの@riverpodを使ってみたい

2023/06/08に公開

何をしたいか

riverpodのgithubを見ると最近@riverpodとつけているのが増えている気がします。
https://github.com/rrousselGit/riverpod/blob/master/examples/counter/lib/main.dart
ただriverpodのページを見ても既存のProviderを作成する方法しか載っていないように見えましたが、ページの上部にThe documentation for version 2.0 is in progress. A preview is available at: https://docs-v2.riverpod.devという文言を発見。

https://docs-v2.riverpod.dev/

どうやら新しいドキュメントを作成しているようなので、それを見ると@riverpodの説明などありそうなのでドキュメントを読み込んでみる。

セットアップ

以下、パッケージを追加する。

flutter pub add flutter_riverpod dev:custom_lint dev:riverpod_lint riverpod_annotation dev:build_runner dev:riverpod_generator

flutter_rivderpodはいつも追加しているが、custom_lint``riverpod_list``rivderpod_annotation``riderpod_generatorは使ったことはなかったです。とりあえず言われるがままに追加しています。

custom_lint/riverpod_lint

custom_lintriverpod_lintはセットで使うっぽいです。(custom_lintの機能でriverpod_listを使うイメージ?🤔)

analysis_options.yamlのファイルに以下を追記する。

analyzer:
  plugins: 
    - custom_lint

こうするとIDE上に警告が出るようになるようでした。

まだいつもはflutter analyzeのコマンドを実行して静的解析をしているけども、custom_lintを利用する場合はdart run custom_lintコマンドを実行するようでした。

~/D/w/r/reverpod_test ❯❯❯ dart run custom_lint
  lib/main.dart:4:3 • Flutter applications should have a ProviderScope widget at the top of the widget tree. • missing_provider_scope

runApp内でProviderScopeを宣言していない的なWarning。
確かにriverpodを利用する前提の静的解析をするようになっていそう👍

とにかく@riverpodを使ってみる

サンプルに書いてあったことをそのまま実装してみます。

part 'main.g.dart'; // build_runnerで出力されるファイル

@riverpod
String helloWorld(HelloWorldRef ref) {
  return 'Hello world';
}

freezedなどを利用している場合は馴染み深いですが、アノテーションを利用するためbuild_runnerのコマンドを実行するようでした。
そのためにpart 'main.g.dart'を宣言し、Hello Worldの文字列を返却する関数に@riverpodをつけました。

その状態でいつものコマンドを実行します。

$ dart run build_runner build -d

helloWorldがどんな形に変換され、出力されたのかを確認してみます。

// **************************************************************************
// RiverpodGenerator
// **************************************************************************

String _$helloWorldHash() => r'8bbe6cff2b7b1f4e1f7be3d1820da793259f7bfc';

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

typedef HelloWorldRef = AutoDisposeProviderRef<String>;
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions

色々実装されているようですが、結果的には以下のhelloWorldProviderの実装をしたのと同じかんじだそうでした。

final helloWorldProvider = AutoDisposeProvider(
  (ref) => 'Hello world',
);

参照方法は以下のような感じで、自前で宣言したときのProviderの使いかたと同じでした。

  Consumer(
    builder: (_, ref, __) {
      final helloWorld = ref.watch(helloWorldProvider);
      return Text(helloWorld);
    },
  ),

クラス定義に@rivderpodをつけてみる

以下のクラスを宣言し、@riverpodをつけてコードを出力してみます。

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
}

dart run build_runner build -d

typedef HelloWorldRef = AutoDisposeProviderRef<String>;
String _$counterHash() => r'4243b34530f53accfd9014a9f0e316fe304ada3e';

/// See also [Counter].
@ProviderFor(Counter)
final counterProvider = AutoDisposeNotifierProvider<Counter, int>.internal(
  Counter.new,
  name: r'counterProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : _$counterHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef _$Counter = AutoDisposeNotifier<int>;

想定通りcounterProviderが生成されました。
ただStateProviderとかではなく、NotifierProviderというものが生成されていました。

参照するときは

final count = ref.watch(counterProvider);

関数を実行するときは

ref.read(counterProvider.notifier).increment();

NotifierProvider以外のProvider

StateNotifierProviderChangeNotifierProviderは非推奨になっているようで、NotifierProviderの利用を検討する必要があるようでした。

SteamProvider

通信などでよく利用されるSteamProviderは以下のように定義すれば作成されました。
Streamに反応しているようです。

@riverpod
Stream<List<String>> chat(ChatRef ref) async* {
  final socket = await Socket.connect('my-api', 4343);
  ref.onDispose(socket.close);

  var allMessages = const <String>[];
  await for (final message in socket.map(utf8.decode)) {
    allMessages = [...allMessages, message];
    yield allMessages;
  }
}

FutureProvider

こちらも通信でよく利用されるFutureProviderを生成するためのコードになります。
Futureに反応してFutureProviderになるようです。

@riverpod
Future<User> fetchUser(FetchUserRef ref, {required int userId}) async {
  final json = await http.get('api/user/$userId');
  return User.fromJson(json);
}

StateProvider

通信中のIndicatorの表示制御だったり、画面全体の状態などを管理するときなどによく利用する(と思っている)StateProviderを作成する場合は@riverpodは利用できず、既存の作成方法で実装する必要があるとのことでした。

final counterProvider = StateProvider<int>((ref) => 0);

void _incrument() {
  // update関数を使用することでインクリメントなどの処理はより簡潔にかけたりしそうです。
  ref.read(counterProvider.notifier).update((state) => state + 1);
}

いろいろ試してみる

同じ名前のProviderになりそうな宣言をしてみる

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
}

@riverpod
int counter(CounterRef ref) => 0;

両方ともcounterProviderになりそうな宣言でコードを生成してみます。
そうすると見事にcounterProviderが2つ生成されました。

コード生成自体は成功しますが、静的解析やコンパイル時にコケそうな感じでした。

複数のProviderを一個のProviderで参照してみる

@riverpod
int countA(CountARef _) => 5;

@riverpod
int countB(CountBRef _) => 8;

@riverpod
int countAB(CountABRef ref) {
  final countA = ref.watch(countAProvider);
  final countB = ref.watch(countBProvider);
  return countA * countB;
}

WidgetRefではなく、CounterARefなどいつもと違う型名でRefを宣言していましたがいつも通りWidgetRefの感覚で利用してもよさそうでした。

Discussion