await をまたいだら、その画面はもう無いかもしれない
非同期処理の完了時、Widget はすでに破棄されていることがある。await をまたいで setState や BuildContext を使う前に、なぜ mounted・context.mounted を確認するのかを短く整理する。
前回、非同期データを AsyncValue で扱いました。今回はその裏側にある、短いけれど見落としがちな
落とし穴を一つ。await の完了を待っている間に、その画面が閉じられているかもしれない、という
問題です。
§ 01GAPawait の前後で世界が変わる
await は、その行で処理を中断し、Future が完了してから続きを再開します。問題は、この
中断している間にも時間が流れていることです。ユーザーは戻るボタンを押して画面を離れるかも
しれず、そのとき Widget は破棄されます。
つまり、await の前に手にしていた「この Widget」「この context」が、await の後でも
有効とは限りません。破棄された後に setState を呼んだり context を使ったりすると、エラーに
なります。
Future<void> _save() async {await repository.save(draft);// ← ここに来たとき、画面はもう閉じられているかもしれないNavigator.of(context).pop();}
§ 02MOUNTEDState はまだ生きているか
StatefulWidget の State には、mounted というフラグがあります。State がツリーに載っている
間は true、dispose() されると false になります1dispose() で mounted は false になる。State オブジェクト自体はメソッド実行中なので存在するが、ツリーから切り離されているため、setState や context からの祖先探索はもう無効になる。。
await をまたいで setState を呼ぶ前に、これを確認します。
Future<void> _load() async {final data = await repository.fetch();if (!mounted) return; // await をまたいだので、生きているか確認setState(() => _data = data);}
この確認を省くと、破棄済みの State を更新しようとして setState() called after dispose()
という、非同期処理でおなじみのエラーが出ます。
§ 03CONTEXT-MOUNTEDこの context はまだ有効か
State の外(ConsumerWidget など、mounted を持たない場所)や、context を使う場合は、
BuildContext.mounted(Flutter 3.7 以降)を確認します。これはその context に対応する要素が
まだツリーに載っているかを表します。
Future<void> _save() async {final ok = await repository.save(draft);if (!context.mounted) return; // await をまたいだので確認if (ok) {Navigator.of(context).pop();} else {ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('保存に失敗しました')),);}}
Navigator.of(context) も ScaffoldMessenger.of(context) も、前回見たとおり context から
祖先をたどる操作です。破棄後の context でこれをやると失敗します。だから await の直後、
context を使う前に context.mounted を確認する——これが定石です。
静的解析もここを見ています。await をまたいで context を使うと use_build_context_synchronously
の警告が出ますが、上のように mounted で早期リターンすれば、正しく解消されます2use_build_context_synchronously は「非同期をまたいで BuildContext を使っている」ことを検知する lint。if (!context.mounted) return; で早期リターンすると、以降の context 使用は安全と判断され警告が消える。。
§ 04SUMMARYawait の後の一行
awaitは完了を待つ間に時間を進める。その間に Widget は破棄されうるStateの中でsetStateする前 …if (!mounted) return;contextを使う前(State の外やcontext経由の操作)…if (!context.mounted) return;- 静的解析の
use_build_context_synchronouslyは、この確認を促している
補足すると、前回の FutureProvider / AsyncValue のようにフレームワークが非同期の生存管理を
引き受ける仕組みを使えば、この確認の多くは不要になります。mounted を意識するのは、ボタン押下
→ await → 画面遷移のような、自分で手続き的に書く非同期ハンドラの場面です。
これで、画面のコードと Provider を「なぜそう書くか」まで含めて読めるようになりました。連載の 最後は、そのコードを守る側——Flutter のテストの種類(unit / widget / E2E の分け方)です。