Reputation: 2344
In my flutter application I have a button which starts an asynchronous network operation and displays a dialog while waiting for it to complete. Once completed, the dialog is popped from the navigation, but I am running into concurrency issues. Here's my code:
ElevatedButton(
onPressed: () async {
showDialog(
context: context,
builder: (context) => Center(child: CircularProgressIndicator()),
);
await asyncNetworkOperation();
Navigator.pop(context);
},
child: Text("Press here");
)
If the user taps on the Android back button while the network operation is in progress, the dialog gets popped ahead of time. And then, once the network operation completes, another Navigator.pop(context) is issued which pushes the navigation back one extra step.
What is the best way to avoid Navigator.pop from executing if the user already popped the dialog? I know I could prevent the back button from working altogether with the WillPopScope
widget, but I would like the user to have the ability to abort the operation should it take too long.
TLDR: How to prevent a Navigator.pop()
in an async frunction from popping a route which has already been popped?
Upvotes: 1
Views: 5003
Reputation: 77
Not sure, if this make sense.
if(ModalRoute.of(dialogContext)?.isCurrent ?? false){
Navigator.pop(dialogContext);
}
If I am not wrong, this is checking the route of the given context is currently on the top of the navigator tree. If it is pop, else dont pop. In this example the context i pass belongs to the dialog. The other way i used was to make sure that the widget under my dialog is not at the top.
//Assume widgetContext is the Scaffold below my dialog
//and dialogContext is the context of my dialog
if(ModalRoute.of(widgetContext)?.isCurrent != true){
Navigator.pop(dialogContext);
}
Null safety, is up to you, I put "!= true" because if its null i do not want to pop as well. for whatever reason the ModalRoute for the widgetContext does not return me a value. same as why i use "?? false"
Upvotes: 0
Reputation: 111
This issue can be solved by using the StatefulWidget. We can use mounted property of a StatefulWidget to check wether the widget is disposed or not. There is no mounted property in StatelessWidget.
ElevatedButton(
onPressed: () async {
showDialog(
context: context,
builder: (context) => Center(child: CircularProgressIndicator()),
);
await asyncNetworkOperation();
if(mounted) {
Navigator.pop(context);
}
},
child: Text("Press here");)
Upvotes: 0
Reputation: 1224
A versatile approach (and probably recommended) is using the mounted property, which would require you to use a StatefulWidget
:
class MyDialog extends StatefulWidget {
const MyDialog({Key? key}) : super(key: key);
@override
State<MyDialog> createState() => _MyDialogState();
}
class _MyDialogState extends State<MyDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('AlertDialog Title'),
content: const Text('AlertDialog description'),
actions: <Widget>[
TextButton(
onPressed: () async {
await Future.delayed(const Duration(seconds: 5));
if (!mounted) {
return;
}
Navigator.pop(context, 'Cancel');
},
child: const Text('Cancel'),
),
],
);
}
}
After creating a State
object and before calling initState
, the framework "mounts" the State object by associating it with a BuildContext
. The State
object remains mounted until the framework calls dispose
, after which time the framework will never ask the State
object to build again, therefore you can check the mounted
property before safely using the context object, for other purposes other than just popping the navigator as well.
Upvotes: 3
Reputation: 4767
You could check if the dialog is active or not. Only go back if there is active dialog
Example:
_isOpen = true;
ElevatedButton(
onPressed: () async {
showDialog(
context: context,
builder: (context) => Center(child: CircularProgressIndicator()),
).then((_) => _isOpen = false);
await asyncNetworkOperation();
if(_isOpen){
Navigator.pop(context);
}
},
child: Text("Press here");
)
Upvotes: 2