【Flutter】Riverpodのv2対応の公式ドキュメントでの気づきをまとめてみた

image

はじめに

10月の中旬ごろにリニューアル中だったRiverpodの公式ドキュメントが完成したようです。
最新版を読み直して気づいたことなどをまとめていければと思います。

riverpod_lint

riverpod_listというRiverpod用のlintルールを適用できるパッケージがあるようです。

  • Consumerでのラップオプションの追加
  • ConsumerWidgetへの変換オプションの追加
  • Riverpod関連のlint設定

ができるようなので、入れておくのが良いと思います。
riverpodの作者も導入を推奨しているようです。

Flutter Riverpod Snippets

VSCodeの拡張機能。
providerの記述が楽になるかもです。   riverpod_generatorを使っている場合はProviderを自ら記述する機会があまりないと思うので、導入するメリットが少なくなるかもです。
https://marketplace.visualstudio.com/items?itemName=robert-brunhage.flutter-riverpod-snippets

3つのAsyncNotifierのStateの更新方法

1. state = AsyncData(response) を使用する

メリット
APIからのレスポンスをそのままstateに入れることができるのであればシンプルに書ける。

デメリット

  • APIからのレスポンスがない場合には話は変わってくる。
  • GET系のAPI側で勝手にフィルタリングやソートを行ってくれている場合などは、フロントエンド側で処理を行わないといけなくなるので、やや複雑になる

Future<void> addTodo(Todo todo) async {
  // The POST request will return a List<Todo> matching the new application state
  final response = await http.post(
    Uri.https('your_api.com', '/todos'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode(todo.toJson()),
  );

  // We decode the API response and convert it to a List<Todo>
  List<Todo> newTodos = (jsonDecode(response.body) as List)
      .cast<Map<String, Object?>>()
      .map(Todo.fromJson)
      .toList();

  // We update the local cache to match the new state.
  // This will notify all listeners.
  state = AsyncData(newTodos);
}

2. ref.invalidateSelf() を使用してproviderを再度初期化する

メリット

  • GET APIのリクエストでフィルタリングやソートなどを行っている場合はこちらが良い

デメリット

  • GET APIなどを再実行するのでパフォーマンスは悪くなるケースがある

Future<void> addTodo(Todo todo) async {
  // We don't care about the API response
  await http.post(
    Uri.https('your_api.com', '/todos'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode(todo.toJson()),
  );

  // Once the post request is done, we can mark the local cache as dirty.
  // This will cause "build" on our notifier to asynchronously be called again,
  // and will notify listeners when doing so.
  ref.invalidateSelf();

  // (Optional) We can then wait for the new state to be computed.
  // This ensures "addTodo" does not complete until the new state is available.
  await future;
}

3. 手動でのStateの更新

メリット

  • 余計なAPIコールがない

デメリット

  • 基本的にDBの情報が正だが、それと異なるデータになってしまう可能性がある(別ユーザーがDBを書き換えていた場合など)

Future<void> addTodo(Todo todo) async {
  // We don't care about the API response
  await http.post(
    Uri.https('your_api.com', '/todos'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode(todo.toJson()),
  );

  // We can then manually update the local cache. For this, we'll need to
  // obtain the previous state.
  // Caution: The previous state may still be loading or in error state.
  // A graceful way of handling this would be to read `this.future` instead
  // of `this.state`, which would enable awaiting the loading state, and
  // throw an error if the state is in error state.
  final previousState = await future;

  // We can then update the state, by creating a new state object.
  // This will notify all listeners.
  state = AsyncData([...previousState, todo]);
}

providerにfamilyをつける場合

autoDisposeをなるべくつけておいた方が良い。
引数毎にインスタンスが生成されるため、メモリリークの原因になりやすい。

注意

同じ引数かどうかの判定にあたり、引数の == オペレータを頼りに判別するため、引数に入れた変数が == オペレータをオーバーライドしていない場合は、同じ引数判定ができない。

具体例

ref.watch(activityProvider(['recreational', 'cooking']));

['recreational', 'cooking'] == ['recreational', 'cooking']falseになるため、同じ引数判定ができない。
=> riverpod_listで警告が出るはず。

invalidateの挙動の違い

  • 引数なしの場合: 全(label)providerがリフレッシュされる
  • 引数ありの場合: 特定のproviderのみがリフレッシュされる

具体例

void onTap() {
  // Invalidate all possible parameter combinations of this provider.
  ref.invalidate(labelProvider);
  // Invalidate a specific combination only
  ref.invalidate(labelProvider('John'));
}

ProviderObserverについて

ProviderObserverを使えば以下の4つイベントが取得できる。

- `didAddProvider`, called when a provider is added to the tree
- `didUpdateProvider`, called when a provider is updated
- `didDisposeProvider`, called when a provider is disposed
- `providerDidFail`, when a synchronous provider throws an error

非同期の(asynchronous)providerの場合はAsyncErrorでなく、didAppProvider,didUpdateProvider側でエラーがキャッチするようになっている
⇒ riverpod3.0以降で修正予定のようです。

ref.refreshとref.invalidateの違い

ref.refresh = invalidate + read。invalidateした後に最新値を取得したい場合はrefreshを使うと良い。

T refresh<T>(provider) {
  invalidate(provider);
  return read(provider);
}

providerの初期化タイミングについて

Widget内のinitStateでなく、遷移処理の直前の方が良い。

Don't

class WidgetState extends State<MyWidget> {
  
  void initState() {
    super.initState();
    // Bad: the provider should initialize itself
    ref.read(provider).init();
  }
}

Do
遷移処理前に初期化しておく。

ElevatedButton(
  onPressed: () {
    ref.read(provider).init();
    Navigator.of(context).push(...);
  },
  child: Text('Navigate'),
)

providerはtop-levelのfinal変数にするべき

Don't

class Example {
  // Unsupported operation. Could cause memory leaks and unexpected behaviors.
  final provider = Provider<String>((ref) => 'Hello world');
}

Do

final provider = Provider<String>((ref) => 'Hello world');

Exampleクラスのインスタンスがプロジェクト内で1つではない場合に、providerがdisposeされずに永遠に残ってしまうとかかな?
シングルトンなどを使っていない場合は、同じインスタンスにアクセスしようと思ってもできなかったり?
ちょっと意図が微妙に掴めていないかも。

Widget内のローカルStateなどの管理にProviderを使うのは避ける

Widget内部のローカルの状態管理はflutter_hooksを使いましょうとのこと。
providerはtop-levelのfinal変数として扱われるべきだという話もあり、他の画面などからローカルのStateにアクセスできてしまうのは良くないという理由でしょう。
providerをプライベート変数にするなどで解決もできそうですが、公式ドキュメント通りに実装するということであれば、flutter_hooksを使う実装の方が良いのかもしれませんね。

ref.watchでselectを使う価値があまりないケース

stateのその他プロパティがほとんど更新されないケース。
selectを使用すると、個々の読み取り操作が若干遅くなり、コードの複雑さがほんの少し増すため、他のプロパティがほとんど変更されないのであれば、selectを使って購読するプロパティを限定する価値がないようです。

最後に

今回はriverpodのリニューアルしたドキュメントで学んだことを列挙してみました。
個人的に新たに学べた場所を列挙しただけであり、人によって学べる内容は違うので、一度じっくり読んでみることを推奨します。
読んでいただいた方に新しい発見があれば幸いです。

参考

https://riverpod.dev/

お知らせ

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More

タグ

Flutter (110)初心者向け (28)イベント (18)Google Apps Script (15)Nextjs (12)可茂IT塾 (8)Firebase (7)riverpod (6)React (6)ChatGPT (5)vscode (5)デザイン (5)新卒 (4)就活 (4)Figma (4)Dart (4)JavaScript (4)お知らせ (4)FlutterWeb (3)Prisma (3)NestJS (3)Slack (3)TypeScript (3)ワーケーション (3)インターン (3)設計 (2)線型計画法 (2)事例 (2)Git (2)Image (2)File (2)Material Design (2)画像 (2)iOS (2)アプリ開発 (2)React Hooks (2)tailwindcss (2)社会人 (2)大学生 (2)RSS (1)Google (1)Web (1)CodeRunner (1)個人開発 (1)Android (1)Unity (1)WebView (1)Twitter (1)フルリモート (1)TextScaler (1)textScaleFactor (1)学生向け (1)supabase (1)Java (1)Spring Boot (1)shell script (1)正規表現 (1)パワーポイント (1)趣味 (1)モンスターボール (1)CSS (1)SCSS (1)Cupertino (1)ListView (1)就活浪人 (1)既卒 (1)保守性 (1)iPad (1)シェアハウス (1)スクレイピング (1)PageView (1)画面遷移 (1)flutter_hooks (1)Gmail (1)GoogleWorkspace (1)ShaderMask (1)google map (1)Google Places API (1)GCPコンソール (1)Google_ML_Kit (1)Vercel (1)Google Domains (1)DeepLeaning (1)深層学習 (1)Google Colab (1)コード生成 (1)GitHub Copilot (1)オンラインオフィス (1)オブジェクト指向 (1)クラスの継承 (1)ポリモーフィズム (1)LINE (1)Bitcoin (1)bitFlyer (1)コミュニティー (1)文系エンジニア (1)Freezed (1)permission_handler (1)flutter_local_notifications (1)markdown (1)GlobalKey (1)ValueKey (1)Key (1)アイコン (1)go_router (1)debug (1)datetime_picker (1)Apple Store Connect (1)FlutterGen (1)デバッグ (1)Widget Inspector (1)検索機能 (1)Shader (1)Navigator (1)メール送信 (1)Firebase App Distribution (1)Fastlane (1)Dio (1)CustomClipper (1)ClipPath (1)カスタム認証 (1)アニメーション (1)Arduino (1)ESP32 (1)経験談 (1)フリーランス (1)mac (1)csv (1)docker (1)GithubActions (1)Dialog (1)BI (1)LifeHack (1)ショートカット (1)Chrome (1)高校生 (1)キャリア教育 (1)非同期処理 (1)生体認証 (1)BackdropFilter (1)レビュー (1)getAuth (1)Algolia (1)コンサルティング (1)Symbol (1)

お知らせ

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More