1. WillPopScope usage
WillPopScope
In essence, a widget is used to intercept physical key return events (Android's physical return key and iOS's side-sliding return). Let's first understand this class. It's very simple. There are two parameters in total, sub-widgetchild
and used to listen for intercepting and return eventsonWillPop
method
const WillPopScope({ , required , required , }) : assert(child != null);
Let's take Android as an example to see the usage. The usage is very simple
body: WillPopScope( child: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Text("back") ), onWillPop: () async { log("onWillPop"); /**Return true. Just like not implementing onWillPop, it will automatically return, *Return false route no longer responds to physical return events, intercepting returns events are handled by themselves */ return false; }, ),
After adding WillPopScope to the page that needs to intercept the return event, when the return value is false, clicking the physical return key page will not respond, and you need to implement the return logic yourself.
2. Problems encountered when using WillPopScope
When there is only one flutter projectNavigator
When using the above method, there is no problem, but there are often multiple projectsNavigator
, we will encounterWillPopScope
The failure situation (the specific principle will be explained later), let’s first look at a nested example
Main page, since MaterialApp is a Navigator, we nest a Navigator in it, and the example only writes the key code
main page
body: WillPopScope( child: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Navigator( onGenerateRoute: (RouteSettings settings) => MaterialPageRoute(builder: (context) { return FirstPage(); }), ) ), onWillPop: () async { print("onWillPop"); /**Return true. Just like not implementing onWillPop, it will automatically return, *Return false route no longer responds to physical return events, intercepting returns events are handled by themselves */ return true; },
First page, embed into the home page, create a route and jump to the second page
class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return WillPopScope( child: Center( child: InkWell( child: const Text("Page 1"), onTap: () { //Skip to the second page (context, MaterialPageRoute(builder: (context) { return SecondPage(); })); }, )), onWillPop: () async { // Listen to the physical return event and print print("first page onWillScope"); return false; }); } }
Page 2
class SecondPage extends StatelessWidget { @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async{ // Listen to the physical return event and print print("second page onWillPop"); return false; }, child: const Center( child: Text("Page 2"), ), ); } }
After running, you will find that only the onWillPop on the home page listens to the physical return event, and the onWillPop on the first and second pages has no response.
I/flutter: onWillPop
It seems that it only responds to the original Navigator, and the listening of the nested Navigator has no effect. Why does such a problem occur? The following is an explanation of the principle of WillPopScope. If you only want to see the solution, please jump directly to the end of the article.
3. WillPopScope principle
Let's first look at the source code of WillPopScope. The main source code of WillPopScope is the following two paragraphs. It is easy to understand. It is to compare whether onWillPop changes and update after the UI or data is updated.
@override void didChangeDependencies() { (); if ( != null) { _route?.removeScopedWillPopCallback(!); } //Get ModalRoute _route = (context); if ( != null) { _route?.addScopedWillPopCallback(!); } } @override void didUpdateWidget(WillPopScope oldWidget) { (oldWidget); if ( != && _route != null) { if ( != null) { _route!.removeScopedWillPopCallback(!); } if ( != null) { _route!.addScopedWillPopCallback(!); } } }
Focus on this paragraph, get ModalRoute and register onWillPop into ModalRoute
_route = (context); if ( != null) { //This method is to put onWillScope into the _willPopCallbacks array held by route _route?.addScopedWillPopCallback(!); }
Enter ModalRoute and see that onWillPop registered in _willPopCallbacks is called in WillPop. Note that when the return value onWillPop is false, the return value of WillPop is.
A small doubt is solved here. The effect of onWillPop returns the value, and if it returns false, it will not pop. But our main doubt has not been resolved yet, so we can only continue to look down.
@override Future<RoutePopDisposition> willPop() async { final _ModalScopeState<T>? scope = _scopeKey.currentState; assert(scope != null); for (final WillPopCallback callback in List<WillPopCallback>.of(_willPopCallbacks)) { if (await callback() != true) { // When the return value is false, doNotPop return ; } } return (); }
Then find the method to call WillPop, which is a MaybePop method, which containsSame Navigator
We will not analyze the pop-up logic of the pages here. Those who are interested can study them by themselves. But if differentNavigator
Woolen cloth? Let's look at this method firstReturn value, this is very important. But our questions cannot be answered here, we can only continue to trace them upwards.
@optionalTypeArgs Future<bool> maybePop<T extends Object?>([ T? result ]) async { final _RouteEntry? lastEntry = _history.cast<_RouteEntry?>().lastWhere( (_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e), orElse: () => null, ); if (lastEntry == null) { return false; } assert(._navigator == this); final RoutePopDisposition disposition = await (); // this is asynchronous assert(disposition != null); if (!mounted) { // Forget about this pop, we were disposed in the meantime. return true; } final _RouteEntry? newLastEntry = _history.cast<_RouteEntry?>().lastWhere( (_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e), orElse: () => null, ); if (lastEntry != newLastEntry) { // Forget about this pop, something happened to our history in the meantime. return true; } switch (disposition) { case : return false; case : pop(result); return true; case : return true; } }
Who called it againmaybePop
What's the way, that'sdidPopRoute
, didPopRoute
The method is located at_WidgetsAppState
middle
@override Future<bool> didPopRoute() async { assert(mounted); // The back button dispatcher should handle the pop route if we use a // router. if (_usesRouterWithDelegates) { return false; } final NavigatorState? navigator = _navigator?.currentState; if (navigator == null) { return false; } return (); }
Based on layers of traceability, we are now coming to the following method. This method is easy to understand and is also something that puzzles me. for loop traversal_observes
All in the arrayWidgetsBindingObserver
。but——Note this transition if the first element in the array isdidPopRoute
Method returntrue
, then the traversal ends, if returnfalse
Then it will be called in the end()
, This method means to exit the application directly. That is to sayhandlePopRoute
This method either executes the first one in the arrayWidgetBindingObserver
ofdidPopRoute
Or exit the app. It feels like this for loop is unchanged.
Then why talk about this method? Because the application will call this method after listening to the physical return key event.
@protected Future<void> handlePopRoute() async { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) { if (await ()) { return; } } (); }
Now we know that the application will call after hearing the physical return key eventhandlePopRoute
method. buthandlePopRoute
Or call_observers
The first item of the arraydidPopRoute
Method, either exit the application. In other words, if you want to listen to the system's return event, you must have a registered _observersWidgetBindingObserver
And it must be_observers
The first element in the array. Search_observers
You can know the relevant operation methods_observers
Adding elements is only usedadd
method, so the first element will never change. So who is the first WidgetBindingObserver? That's what's mentioned above_WidgetsAppState
, and_WidgetsAppState
Will hold oneNavigatorKey
,thisNavigatorKey
It is the application in the first placeNavigator
holder.
In summary, we understand the application's physical return key listening logic, which will only call the application's first Navigator, so all our listening return logic can only be implemented in the system's first Navigator. So what should we do with nested Navigator?
4. Solutions for nested Navigator not to listen to physical return keys
Since you cannot directly handle physical return events of nested Navigator, you can only save the country in a curve. First remove the invalid onesWillPopScope
。
first page
class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: InkWell( child: const Text("Page 1"), onTap: () { (context, MaterialPageRoute(builder: (context) { return SecondPage(); })); }, )); } }
second page
class SecondPage extends StatelessWidget { @override Widget build(BuildContext context) { return const Center( child: Text("Second page"), ); } }
The highlight comes to the main page, oronWillPop
Set to false. Intercept all physical return events. Just set one for NavigatorGlobalKey
, and thenonWillPop
Implement the return logic of the corresponding navigator.
class MyHomePage extends StatefulWidget { const MyHomePage({, required }); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { GlobalKey<NavigatorState> _key = GlobalKey(); return Scaffold( appBar: AppBar( title: Text(), ), body: WillPopScope( child: Center( child: Navigator( key: _key, onGenerateRoute: (RouteSettings settings) => MaterialPageRoute(builder: (context) { return FirstPage(); }), ) ), onWillPop: () async { print("onWillPop"); if(_key.currentState != null && _key.currentState!.canPop()) { _key.currentState?.pop(); } /**Return true. Just like not implementing onWillPop, it will automatically return, *Return false route no longer responds to physical return events, intercepting returns events are handled by themselves */ return false; }, ), ); } }
The above is a detailed explanation of the principle example of the Flutter WillPopScope interception return event. For more information about the Flutter WillPopScope interception return, please pay attention to my other related articles!