Michele
Michele

Reputation: 2344

How to prevent a Navigator.pop() in an async function from popping a route which doesn't exist anymore?

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

Answers (4)

Chung Heon
Chung Heon

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

Arunachalam Ganesan
Arunachalam Ganesan

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

void void
void void

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.


See:
  1. https://dart-lang.github.io/linter/lints/use_build_context_synchronously.html
  2. https://api.flutter.dev/flutter/widgets/State/mounted.html

Upvotes: 3

Sujan Gainju
Sujan Gainju

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

Related Questions