🗂

Bad state: Cannot use "ref" after the widget was disposed. の回避方法

に公開

はじめに

株式会社Sally 所属エンジニアの @wellPicker です。
弊社では、スマホやパソコンでマダミスを遊べるアプリであるウズや、マダミス情報・予約管理サイトマダミス.jp、マダミス開発ツールウズスタジオを開発しています。
マダミスって何? という方はこちらをご覧ください。

本題

Riverpodを使用したことがあれば、一度は下記のようなエラーに遭遇したことがあるのではないかと思います。

Bad state: Cannot use "ref" after the widget was disposed.

このエラーは、Riverpodを使ったFlutterアプリで非同期処理を行う際に発生することがあります。

一般的には「非同期処理の中でrefを使用するのを避けるべき」「if (context.mounted)などでラップすべき」という意見が主流です。僕自身も、可能であればそれらの方針に従うべきだと思います。

しかしこの方針だと、contextが破棄された時はrefを参照する処理が実行されないというデメリットがあります。

都合上どうしても非同期処理内でrefを参照したいし、contextが破棄されてしまった場合も必ず処理を実行したいというケースがあるかもしれません。

そこで、上記の方針とは異なる、「contextが破棄された後でもrefを参照する処理を実行する」回避方法を紹介したいと思います。

エラーを引き起こすパターン

問題が発生する可能性があるコードを簡略化すると、例えば以下のようになります。

  class SomeButton extends ConsumerWidget {
    
    Widget build(BuildContext context, WidgetRef ref) {
      return ElevatedButton(
        onPressed: () async {
          // 何らかの非同期処理
          await someAsyncOperation();

          // 処理成功後、Providerの状態を更新
          ref.read(someProvider.notifier).state = true;
        },
        child: Text('実行'),
      );
    }
  }

一見問題なさそうに見えますが、「someAsyncOperation() の実行中にユーザーが画面を閉じた」などの理由でrefが無効になった場合、エラーが発生します。

なぜこのエラーが起きるのか

ref.read(someProvider.notifier).state = true;で参照しているrefは、ConsumerWidgetによって提供されるWidgetRefを使用しています。
WidgetRefは、それを生成したWidgetのライフサイクルに紐づいています。
Widgetがdisposeされると、そのWidgetから取得したrefは使用できなくなります。

問題のコードでは以下のような流れでエラーが発生します:

  1. ユーザーがボタンを押す
  2. someAsyncOperation() が実行される(非同期処理開始)
  3. ユーザーが画面を閉じる(Widgetがdispose)
  4. someAsyncOperation() が完了する
  5. ref.read(...) を呼び出す → エラーになる

非同期処理の完了を待っている間にWidgetがdisposeされると、その後のrefを使った処理が失敗するというわけです。

解決策

この問題を解決するには、非同期処理が始まる前に、Providerの更新処理への参照を取得しておくという方法が有効です。

1.StateProviderをNotifierに変更

まず、StateProvider を使っている場合は Notifier に変更し、状態を更新するメソッドを定義します。

変更前: StateProvider

  final someProvider = StateProvider<bool>((ref) => false);

変更後: Notifier

  class SomeNotifier extends Notifier<bool> {
    
    bool build() => false;

    void update() {
      state = true;
    }

    void reset() {
      state = false;
    }
  }

  final someProvider = NotifierProvider<SomeNotifier, bool>(
    SomeNotifier.new,
  );

2.非同期処理の前にメソッド参照を取得

次に、Widget側で非同期処理が始まる前にメソッドへの参照を取得しておきます。

  class SomeButton extends HookConsumerWidget {
    
    Widget build(BuildContext context, WidgetRef ref) {
      // ポイント: buildメソッド内で先にメソッド参照を取得
      final update = ref.read(someProvider.notifier).update;

      return ElevatedButton(
        onPressed: () async {
          // 何らかの非同期処理
          await someAsyncOperation();

          // 取得しておいたメソッド参照を使う
          update();
        },
        child: Text('実行'),
      );
    }
  }

この方法が有効な理由は、update はNotifierクラスのメソッドへの参照であり、内部ではProviderRefを参照しています。ProviderRefはWidgetのライフサイクルとは独立であり、WidgetがdisposeされてもNotifier自体は生きているため、メソッドを呼び出すことができます。
ただし、Providerそのものが破棄されてしまうとProviderRefは参照できなくなってしまいます。そのため、たとえばautoDisposeなどを同時に使用する場合は注意が必要です。

応用:コールバックとして渡すケース

ボタンのコールバックとして onSuccess のような形で渡す場合も同様です。

❌ NG: コールバック内でrefを使う

  onSuccess: () => ref.read(someProvider.notifier).state = true,

✅ OK: 事前に取得したメソッド参照を渡す

  final update = ref.read(someProvider.notifier).update;
  // ...
  onSuccess: update,

まとめ

今回は、Riverpodで非同期処理後に ref を使おうとしてエラーになる問題と、解決方法の一例を解説しました。

繰り返しになりますが、非同期処理内でのrefの使用は可能であれば回避する方が望ましいです。ただし、どうしても非同期処理内でrefを参照する処理を実行しなければいけない場合、この記事を参考にWidgetRefを参照しない方法を検討してみてください。

UZU テックブログ

Discussion