【Flutter】「Do not use BuildContexts across async gaps」という警告を消す方法

目的

Flutter 3.0 になってからかどうかは定かではありませんが、とりあえず「Do not use BuildContexts across async gaps」という警告が出るようになりました。この記事では、その理由と解決方法を記載します。

問題

以下のようなソースをStatefulWidgetのStateに書くと、「Do not use BuildContexts across async gaps」という警告が記載されます

  void onTapBad(BuildContext context) async { 
    await Future.delayed(const Duration(milliseconds: 10)); 
    // Do not use BuildContexts across async gaps 
    GoRouter.of(context).push('/test'); 
  } 

use_build_context_synchronouslyによると、以下の通りです。

BuildContextを後で使うために保存しておくと、診断が難しいクラッシュを引き起こしやすくなります。非同期ギャップは暗黙のうちにBuildContextを保存しているため、コードを書く際に見落としがちなポイントです。
BuildContextをStatefulWidgetから使用する場合、非同期ギャップの後にmountedプロパティをチェックする必要があります。

非同期メソッド内でwaitがあった場合、contextの状態が変わってしまうので、その前にmountedプロパティをチェックしてね、ということかな、と思います。私がアプリを作っているときに、contextを引数にしたメソッドを使おうとしたら、その前に画面遷移しちゃってcontextにアクセスできなくて、アプリが落ちた、ということがありました。そういうのの対策かな、と勝手に思ってます。

解決策1

上記のサイトにあるソースを参考にすると、以下のように修正できます。

  void onTapGood2(BuildContext context) async { 
    await Future.delayed(const Duration(milliseconds: 10)); 
    if (!context.mounted) return; 
    GoRouter.of(context).push('/test'); 
  } 
【修正履歴】「mounted」が「context.mounted」に変わっているので修正(2024/02/08)

解決方法2

ただ、mountedはState内にないといけない。私はViewModelにメソッドの本体を入れるため、この方法だと実装の変更が必要になる。もう少し検索すると、以下のようにも書けるとあった。

View側

          OutlinedButton( 
            child: const Text('押す'), 
            onPressed: () => onTapGood3(() { 
              GoRouter.of(context).push('/test'); 
            }), 
          ), 

ViewModel側

  void onTapGood3(void Function() onSuccess) async { 
    await Future.delayed(const Duration(milliseconds: 10)); 
    onSuccess(); 
  } 

全ソース

import 'package:flutter/material.dart'; 
import 'package:go_router/go_router.dart'; 
class TestPage extends StatefulWidget { 
  static const path = '/test'; 
  const TestPage({Key? key}) : super(key: key); 
  @override 
  State createState() => _TestPageState(); 
} 
class _TestPageState extends State { 
  String value = ''; 
  @override 
  Widget build(BuildContext context) { 
    return Scaffold( 
      backgroundColor: Colors.transparent, 
      body: Column( 
        children: [ 
          OutlinedButton( 
              child: const Text('押す'), onPressed: () => onTapGood1(context)), 
          OutlinedButton( 
              child: const Text('押す'), onPressed: () => onTapBad(context)), 
          OutlinedButton( 
              child: const Text('押す'), onPressed: () => onTapGood2(context)), 
          OutlinedButton( 
            child: const Text('押す'), 
            onPressed: () => onTapGood3(() { 
              GoRouter.of(context).push('/test'); 
            }), 
          ), 
        ], 
      ), 
    ); 
  } 
  void onTapGood1(BuildContext context) { 
    GoRouter.of(context).push('/test'); 
  } 
  void onTapBad(BuildContext context) async { 
    await Future.delayed(const Duration(milliseconds: 10)); 
    // Do not use BuildContexts across async gaps 
    GoRouter.of(context).push('/test'); 
  } 
  void onTapGood2(BuildContext context) async { 
    await Future.delayed(const Duration(milliseconds: 10)); 
    if (!mounted) return; 
    GoRouter.of(context).push('/test'); 
  } 
  void onTapGood3(void Function() onSuccess) async { 
    await Future.delayed(const Duration(milliseconds: 10)); 
    onSuccess(); 
  } 
}

まとめ

ということで、解決方法はわかりました。ただ、画面遷移をViewModelに書いているので、この解決方法2の場合、そこは変更が必要になります。もう少しなんかいい方法がないか考えつつ、ひとまず警告を出しっぱなしにしておきます

参考