🔌

driftでref.invalidateSelf()を使う

に公開

強制的に更新する

driftを使用した学習用のサンプルアプリを使って実験してたのですが、データベースからStreamBuilderでデータをリアルタイムに取得していれば、保存・削除の処理を実行したときに画面が変わると思ったのですが変わりませんでした😇

前回シングルトンでやってみたけど、Riverpodの方が慣れているのでリファクタリングして試してみました。
シングルトンを使った記事
Riverpodを使ってないときの方法

こちらが完成品

setState()使ったらできる。でもやりたくない💦
StreamNotifierを使うことにした。
https://riverpod.dev/ja/docs/concepts/about_code_generation#非コード生成バリアントからの移行

Cloud Firestoreでも使ってみたことある。
https://zenn.dev/joo_hashi/articles/861a74988fa687

このままだと表示するだけになる。保存ボタン・🗑️ボタンを押したときに画面を更新してほしいのですよね。

更新するときは、invalidateを使うことが多かったのですが、invalidateSelfを今回使いました。

invalidateについては過去に記事を書いてみました

ref.invalidateSelf()について調べてみる🔍

invalidateSelf abstract method

プロバイダの状態を無効にし、リフレッシュさせる。

リフレッシュは即時ではなく、次の読み取りまたは次のフレームまで遅延されます。

invalidateSelfを複数回呼び出すと、プロバイダは1回だけリフレッシュされます。

invalidateSelfを呼び出すと、プロバイダは直ちに破棄されます。


使ってみた感想ですが、やってくれていることはref.invalidate()と同じに見えました。ボタンを押したら、StreamNotifier/AsyncNotifierでも更新されてるように見えた。翻訳すると更新とは書いてないから違うのだろうな。。。
「画面が変化する」と覚えておくか。

保存・削除ボタンを押す -> 画面にデータが増える/画面からデータが消える

ref.invalidate()の場合だと...

invalidate abstract method

void invalidate( ProviderOrFamily プロバイダ )
プロバイダの状態を無効にし、状態を直ちに破棄して、将来のある時点でプロバイダを再構築させます。

リフレッシュとは対照的に、再構築は即時ではなく、代わりに未定義の時間だけ遅れます。通常、リビルドはイベントループの次のティックで発生する。しかし、プロバイダがリッスンされていない場合、リビルドはプロバイダが再びリッスンされるまで遅れることがある。

invalidateを複数回呼び出すと、状態の再計算が1回行われる。

初期化または破棄されていないプロバイダで使用された場合、このメソッドは何の効果もありません。


再構築と書いてある。解説は違うな。でも同じな気もする。増減、増える・減る。。。
作り直されて表示されるから、再構築されているのでは。

ローカルDBで使うコード

ヘキサゴナルアーキテクチャに最近興味があって真似して作ったコードに書いてます。アダプターと呼ばれているところに、StreamNotifierを置いてます。ViewModelがアダプターなのか。NotifierというかRiverpodは、ViewModelではないという人もいたりする。AndroidのViewModelとMicrosoftのViewModelでは配置のやり方や使い方が違うみたい。

いつもはボタンのCallbackの中に、ref.read()ref.invalidate()を書いて実行していたのですが、メソッドのreturnの上の方に、ref.invalidateSelf()を配置すれば綺麗なコードになっているようには思えた。

昔からこの書き方でやっていたので他に方法ないかなーと悩んでいた💦

async {
  await ref.read();
  ref.invalidate()
}

driftで使用した例はこちら

example
import 'package:drift_tutorial/data/database.dart';
import 'package:drift_tutorial/data/provider/database_provider.dart';
import 'package:drift_tutorial/domain/ports/todo_port.dart';
import 'package:drift/drift.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'todo_adapter.g.dart';

(keepAlive: true)
class TodoAdapter extends _$TodoAdapter implements TodoPort {
  AppDatabase get _db => ref.read(databaseProvider);

  
  Stream<List<TodoItem>> build() {
    return _db.getAllTodoItems();
  }

  
  Stream<List<TodoItem>> getAllTodoItems() {
    return _db.getAllTodoItems();
  }

  
  Stream<List<TodoItem>> getAllTodoItemsIncludeDeleted() {
    return _db.getAllTodoItemsIncludeDeleted();
  }

  
  Future<int> createTodo({
    required String title,
    required String content,
  }) async {
    final now = DateTime.now();
    final result = await _db.createTodoItem(
      TodoItemsCompanion(
        title: Value(title),
        content: Value(content),
        createdAt: Value(now),
        updatedAt: Value(now),
      ),
    );
    ref.invalidateSelf();
    return result;
  }

  
  Future<bool> updateTodo({
    required int id,
    String? title,
    String? content,
  }) async {
    final now = DateTime.now();
    final result = await _db.updateTodoItem(
      TodoItemsCompanion(
        id: Value(id),
        title: title != null ? Value(title) : const Value.absent(),
        content: content != null ? Value(content) : const Value.absent(),
        updatedAt: Value(now),
      ),
    );
    ref.invalidateSelf();
    return result;
  }

  
  Future<int> softDeleteTodo(int id) async {
    final result = await _db.softDeleteTodoItem(id);
    ref.invalidateSelf();
    return result;
  }

  
  Future<int> deleteTodo(int id) async {
    final result = await _db.deleteTodoItem(id);
    ref.invalidateSelf();
    return result;
  }
}

setStateだけの場合

使うことあるのですが、使いすぎると良くなかったりする理由を書いてみました。インターネットで検索したらよく出てくる内容ですね。

  1. 再ビルドの発生
  • setStateを呼び出すたびに、widgetツリーの再ビルドが発生します
  • 特に大きなwidgetツリーの場合、パフォーマンスに影響を与える可能性があります
  1. コードの可読性低下
setState(() {
  _counter++;
  _message = "Updated";
  // ネストが深くなりやすい
  if (_counter > 10) {
    _status = Status.completed;
  }
});
  1. ステート管理の分散
  • 複数のsetStateが異なる場所に散らばると、状態の変更を追跡しづらくなります
  • デバッグが難しくなります
  1. 副作用の管理が難しい
setState(() {
  _isLoading = true;
  // 非同期処理との組み合わせが複雑になりやすい
  fetchData().then((_) {
    setState(() {
      _isLoading = false;
    });
  });
});

テキストだけだと分かりずらいですよね(^_^;)
私の体験だとこんな問題が過去に起きましたね。

  1. 画面が固まる
  2. カクカクする
  3. アプリが停止する。アプリが落ちる。クラッシュするって表現ですかね。
  4. 使うと職場の人に嫌がられる💦なので、flutter_hooks + hooks_riverpodで私はやります。これ使う現場が多いのはReactのエンジニアからFlutterエンジニアにスキルチェンジしたからなんでしょうね。

最後に

最近また個人開発や自己学習でFlutterアプリを色々作ってますが、Riverpodがないと不便だなと感じることが多かったです。いつも決まった機能しか使っていないので、普段使わないライブラリやデモアプリを作って実験してみると使ったことない機能を知ることができるので良い学びになりました。

https://riverpod.dev/ja/docs/essentials/faq
https://www.memory-lovers.blog/entry/2023/12/02/103015
https://www.docswell.com/s/riscait/ZRX97R-riverpod3-and-tips4#p1

Discussion