SoFunction
Updated on 2025-03-11

Detailed explanation of the principle example of the Intercept Return Event

1. WillPopScope usage

WillPopScopeIn 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-widgetchildand used to listen for intercepting and return eventsonWillPopmethod

 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 projectNavigatorWhen using the above method, there is no problem, but there are often multiple projectsNavigator, we will encounterWillPopScopeThe 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 NavigatorWe will not analyze the pop-up logic of the pages here. Those who are interested can study them by themselves. But if differentNavigatorWoolen 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 againmaybePopWhat's the way, that'sdidPopRoute, didPopRouteThe method is located at_WidgetsAppStatemiddle

@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_observesAll in the arrayWidgetsBindingObserverbut——Note this transition if the first element in the array isdidPopRouteMethod returntrue, then the traversal ends, if returnfalseThen it will be called in the end(), This method means to exit the application directly. That is to sayhandlePopRouteThis method either executes the first one in the arrayWidgetBindingObserverofdidPopRouteOr 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 eventhandlePopRoutemethod. buthandlePopRouteOr call_observersThe first item of the arraydidPopRouteMethod, either exit the application. In other words, if you want to listen to the system's return event, you must have a registered _observersWidgetBindingObserverAnd it must be_observersThe first element in the array. Search_observersYou can know the relevant operation methods_observersAdding elements is only usedaddmethod, so the first element will never change. So who is the first WidgetBindingObserver? That's what's mentioned above_WidgetsAppState, and_WidgetsAppStateWill hold oneNavigatorKey,thisNavigatorKeyIt is the application in the first placeNavigatorholder.

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, oronWillPopSet to false. Intercept all physical return events. Just set one for NavigatorGlobalKey, and thenonWillPopImplement the return logic of the corresponding navigator.

class MyHomePage extends StatefulWidget {
  const MyHomePage({, required });
  final String title;
  @override
  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();
}
class _MyHomePageState extends State&lt;MyHomePage&gt; {
  @override
  Widget build(BuildContext context) {
    GlobalKey&lt;NavigatorState&gt; _key = GlobalKey();
    return Scaffold(
      appBar: AppBar(
        title: Text(),
      ),
      body: WillPopScope(
        child: Center(
          child: Navigator(
            key: _key,
            onGenerateRoute: (RouteSettings settings) =&gt; MaterialPageRoute(builder: (context) {
              return FirstPage();
            }),
          )
        ),
        onWillPop: () async {
          print("onWillPop");
          if(_key.currentState != null &amp;&amp; _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!