目的
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');
}
解決方法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の場合、そこは変更が必要になります。もう少しなんかいい方法がないか考えつつ、ひとまず警告を出しっぱなしにしておきます