Waiting asynchronously for Navigator.push() - linter warning appears: use_build_context_synchronously
Short answer:
It's NOT SAFE to always ignore this warning, even in a Stateless Widget.
A workaround in this case is to use the context
before the async call. For example, find the Navigator
and store it as a variable. This way you are passing the Navigator
around, not passing the BuildContext
around, like so:
onPressed: () async {
final navigator = Navigator.of(context); // store the Navigator
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Dialog Title'),
),
);
navigator.pop(); // use the Navigator, not the BuildContext
},
Long answer:
This warning essentially reminds you that, after an async call, the BuildContext might not be valid anymore. There are several reasons for the BuildContext to become invalid, for example, having the original widget destroyed during the waiting, could be one of the (leading) reasons. This is why it's a good idea to check if your stateful widget is still mounted.
However, we cannot check mounted
on stateless widgets, but it absolutely does not mean they cannot become unmounted during the wait. If conditions are met, they can become unmounted too! For example, if their parent widget is stateful, and if their parent triggered a rebuild during the wait, and if somehow a stateless widget's parameter is changed, or if its key is different, it will be destroyed and recreated. This will make the old BuildContext invalid, and will result in a crash if you try to use the old context.
To demonstrate the danger, I created a small project. In the TestPage (Stateful Widget), I'm refreshing it every 500 ms, so the build function is called frequently. Then I made 2 buttons, both open a dialog then try to pop the current page (like you described in the question). One of them stores the Navigator before opening the dialog, the other one dangerously uses the BuildContext after the async call (like you described in the question). After clicking a button, if you sit and wait on the alert dialog for a few seconds, then exit it (by clicking anywhere outside the dialog), the safer button works as expected and pops the current page, while the other button does not.
The error it prints out is:
[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe. At this point the state of the widget's element tree is no longer stable. To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method. #0 Element._debugCheckStateIsActiveForAncestorLookup. (package:flutter/src/widgets/framework.dart:4032:9) #1 Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4046:6) #2 Element.findAncestorStateOfType (package:flutter/src/widgets/framework.dart:4093:12) #3 Navigator.of (package:flutter/src/widgets/navigator.dart:2736:40) #4 MyDangerousButton.build. (package:helloworld/main.dart:114:19)
Full source code demonstrating the problem:
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home Page')),
body: Center(
child: ElevatedButton(
child: Text('Open Test Page'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => TestPage()),
);
},
),
),
);
}
}
class TestPage extends StatefulWidget {
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
late final Timer timer;
@override
void initState() {
super.initState();
timer = Timer.periodic(Duration(milliseconds: 500), (timer) {
setState(() {});
});
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final time = DateTime.now().millisecondsSinceEpoch;
return Scaffold(
appBar: AppBar(title: Text('Test Page')),
body: Center(
child: Column(
children: [
Text('Current Time: $time'),
MySafeButton(key: UniqueKey()),
MyDangerousButton(key: UniqueKey()),
],
),
),
);
}
}
class MySafeButton extends StatelessWidget {
const MySafeButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text('Open Dialog Then Pop Safely'),
onPressed: () async {
final navigator = Navigator.of(context);
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Dialog Title'),
),
);
navigator.pop();
},
);
}
}
class MyDangerousButton extends StatelessWidget {
const MyDangerousButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text('Open Dialog Then Pop Dangerously'),
onPressed: () async {
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Dialog Title'),
),
);
Navigator.of(context).pop();
},
);
}
}
Schnodderbalken
I am the liquid that comes out of your nose. My shape is a timber. In Germany we call that thing SCHNODDERBALKEN!
Updated on January 01, 2023Comments
-
Schnodderbalken 10 months
In Flutter, all
Navigator
functions that push a new element onto the navigation stack return aFuture
as it's possible for the caller to wait for the execution and handle the result.I make heavy use of it e. g. when redirecting the user (via
push()
) to a new page. As the user finishes the interaction with that page I sometimes want the original page to alsopop()
:onTap: () async { await Navigator.of(context).pushNamed( RoomAddPage.routeName, arguments: room, ); Navigator.of(context).pop(); },
A common example is the usage of a bottom sheet with a button with a sensitive action (like deleting an entity). When a user clicks the button, another bottom sheet is opened that asks for the confirmation. When the user confirms, the confirm dialog is to be dismissed, as well as the first bottom sheet that opened the confirm bottom sheet.
So basically the
onTap
property of the DELETE button inside the bottom sheet looks like this:onTap: () async { bool deleteConfirmed = await showModalBottomSheet<bool>(/* open the confirm dialog */); if (deleteConfirmed) { Navigator.of(context).pop(); } },
Everything is fine with this approach. The only problem I have is that the linter raises a warning: use_build_context_synchronously because I use the same
BuildContext
after the completion of anasync
function.Is it safe for me to ignore / suspend this warning? But how would I wait for a push action on the navigation stack with a follow-up code where I use the same
BuildContext
? Is there a proper alternative? There has to be a possibility to do that, right?PS: I can not and I do not want to check for the
mounted
property as I am not usingStatefulWidget
. -
Schnodderbalken about 2 yearsExcellent explanation and understandable minimal example! Thank you. The bounty is yours :). One question though: you label your solution as a "quick workaround". Does it mean that there is a cleaner solution?
-
WSBT about 2 years@Schnodderbalken Haha, I was mostly focusing on the word "quick" - in your case (of getting the navigator), this is already a very clean solution. However we might not always be this lucky. For example, if you have a custom function that takes in a BuildContext as a parameter, this "workaround" won't work, so you may have to do some not-so-quick refactoring or think of another workaround. BTW, excellent question tho, it's my first time hearing about this lint rule. Hopefully your question can help more people once this lint is released as a default rule.
-
Schnodderbalken about 2 yearsI am using the community-driven lint package called
lint
: pub.dev/packages/lint. This package is more strict than the official one and so I came across this warning. Anyways, thank you again. Have the bounty now ;) -
Matthew Sisinni over 1 yearI'd like to add that the linter might not catch instances where context is used asynchronously in a Future's .then() or .whenComplete() blocks, but using the context in those blocks may still lead to an error.
-
Wesley Barnes over 1 yearChecking if mounted seems to be available inside my Stateless widget -> if (!navigator.mounted) return; Could this maybe solve the issue commented above that you cannot check if mounted in stateless widgets?
-
markhorrocks over 1 year
final navigator = Navigator.of(context);
produces a linter error. -
WSBT over 1 year@markhorrocks What's the linter warning message and which lint rule is that?
-
markhorrocks over 1 yearThe lint warning is Do not use context across async gaps. On reflection this case may not apply as it is not an async function.
-
Masahiro Aoki over 1 yearGreat answer! I made a dartpad based on your sample code. dartpad.dev/?id=0215a7e3c67347c84450697c824842ef