Catching Back Button Presses on Android using Navigator 2.0
A Flutter Case Study
Welcome to the first in a new series of articles on Flutter. I have recently started browsing StackOverflow for Flutter related that I might be able to answer in an effort to help the community of budding Flutter enthusiasts. Every now and then, I come across a question that I feel warrants an article explaining the problem and solution, and perhaps a few details as to why the issue arises to begin with. I have dubbed these articles Case Studies, and I look forward into digging into the intriguing, obscure, or far-too-common issues that I come across. So without further ado, let’s dive into our first case study.
If you found this article worthy of a read from the title, then you have undoubtedly had experience catching back button presses on Android in Flutter pre-Navigator 2.0, but here’s a little refresher. We simply wrap our widget tree in a WillPopScope
widget and provide an onWillPop
callback. Here is an example that catches the back button press and provides an AlertDialog
to confirm app exit.
WillPopScope(
onWillPop: () async {
final shouldPop = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Exit App'),
content: Text('Are you sure you want to leave the app?'),
actions: [
TextButton(
child: Text('Cancel'),
onPressed: () => Navigator.pop(context, false),
),
TextButton(
child: Text('Confirm'),
onPressed: () => Navigator.pop(context, true),
),
],
);
},
); // if the dialog is dismissed by tapping outside of the barrier
// the result is null, so we return false
return shouldPop ?? false;
},
child: // continue with widget tree here
)
As you can see, the user’s input in the AlertDialog
will tell us whether they actually want to leave the app, and we can pass that result along to the WillPopScope
widget.
If you’ve tried this approach while using Navigator 2.0, then you’ve discovered that WillPopScope
no longer catches back button presses. This is because with Navigator 2.0, RouterDelegate
's popRoute
method is responsible for handling requests from the operating system to pop the current route (back button presses), and we can see in the method’s documentation:
The method should return a boolean [Future] to indicate whether this delegate handles the request. Returning false will cause the entire app to be popped.
The implementation by design pops the entire app immediately upon a press of the back button. Therefore, we must override it when extending the RouterDelegate
class and handle back button presses according to our needs.
@override
Future<bool> popRoute() async {
// check if we have pages in the stack to pop before
// attempting app exit
if (_pages.length > 1) {
// handle popping the current page off of the stack
return Future.value(true);
} final result = await showDialog<bool>(
// get the context from the navigatorKey defined
// in your RouterDelegate class
context: navigatorKey!.currentContext!,
builder: (context) {
return AlertDialog(
title: const Text('Exit App'),
content: const Text('Are you sure you want to exit the app?'),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.pop(context, true),
),
TextButton(
child: const Text('Confirm'),
onPressed: () => Navigator.pop(context, false),
),
],
);
},
); // if the dialog is dismissed by tapping outside of the barrier
// the result is null, so we return false
return shouldPop ?? false;
}
If you do not need back button presses to navigate backwards in your app and need it to present the exit app dialog at all times, you may omit the first if statement.
NOTE: You may have noticed that the results from the dialog are reversed in the two approaches. This is because when using WillPopScope
, we are telling the widget if it should pop the scope from the Navigator
(true = pop, false = don’t pop). When using Navigator 2.0, we are telling the Router
whether or not we have handled the request (true = handled, false = not handled). When we report that we have not handled it ourselves, the Router
handles it by exiting the app.
That’s all there is to it. By moving your logic from WillPopScope
's onWillPop
callback, to RouterDelegate
's popRoute
method, you can easily handle back button presses while using Navigator 2.0.
If you would like a deeper dive into Navigator 2.0 with an in-depth tutorial on converting an app to Navigator 2.0, check out my 3 part series starting with A Simpler Guide to Flutter Navigator 2.0: Part I.
Thank you for reading! If you found this article helpful and would like to read more case studies, please clap and follow. Happy coding!