Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use WillPopScope inside a Navigator in Flutter?

Tags:

flutter

dart

WillPopScope's callback isn't being fired when I expected it to be. How can I use WillPopScope inside a Navigator so a route can handle it's own back pressed behaviour.

I'm learning to use flutter, and I came across Navigator for creating multiple pages (I know there are other navigation widgets but I'm after something which only supports programatic navigation so I can handle all the UI).

The next thing I thought to look at with the Navigator was going back, I found WillPopScope which wraps a component and has a callback that gets called when the back button is pressed (if the component is rendered). This seemed ideal for me since I only want the callback to be called if the Widget is rendered.

I tried to use WillPopScope within a Navigator with the intention for only the rendered route to have it's callback (onWillPop) called when the back button is pressed, but putting WillPopScope within a Navigator does nothing (the callback isn't called).

The intention is to have a Navigator navigate to top level routes and those routes themselves potentially having Navigators, so putting WillPopScope inside means each route (or subroute) is responsible for it's own back navigation.

Many questions I've looked up seem to focus on MaterialApp, Scaffold, or other ways of handling navigation; I'm looking how to handle this without the UI that those things bring in (a use case could be a quiz app, where you need to click a next button to move forward, or something similar).

Here is the minimal main.dart file I expect route 2 to handle it's own back navigation (to keep things simple I've not put nested routes in this example).

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Navigation',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext parentContext) {
    return Container(
        color: Theme.of(context).colorScheme.primary,
        child: Navigator(
            initialRoute: "1",
            onGenerateRoute: (settings) {
              return PageRouteBuilder(pageBuilder: (BuildContext context,
                  Animation animation, Animation secondaryAnimation) {
                switch (settings.name) {
                  case "1":
                    return Container(
                        color: Colors.red,
                        child: GestureDetector(onTap: () {
                          debugPrint("going from 1 to 2");
                          Navigator.of(context).pushNamed("2");
                        }));
                  case "2":
                    return WillPopScope(
                      child: Container(color: Colors.green),
                      onWillPop: ()async {
                        debugPrint("popping from route 2 disabled");
                        return false;
                      },
                    );
                  default:
                    throw Exception("unrecognised route \"${settings.name}\"");
                }
              });
            })
    );
  }
}

When the back button is pressed in either route 1 or 2, the app exits. I expect that to only be the case in route 1, and route 2 should only log popping from route 2 disabled (with no navigation away from the page or leaving the app).

From what I understand, Navigator and WillPopScope are the Widgets to use for this sort of thing, but if not then how would I implement self contained (potentially nested) routes.

like image 435
Jonathan Cowling Avatar asked Jun 21 '19 16:06

Jonathan Cowling


2 Answers

You can forward onWillPop to another navigator by using below code:

onWillPop: () async {
    return !await otherNavigatorKey.currentState.maybePop();
}
like image 52
mix1009 Avatar answered Oct 13 '22 13:10

mix1009


When you create a Navigator you are automatically creating a new stack. Everything you .push below your Navigator using Navigator.of(context) is added to the new Navigator stack you just created. However, when you press the backbutton it doesn't know what you want to pop (if the root navigator or the new navigator).

First you need to add a WillPopScope outside your Navigator and add a NavigatorKey to your Navigator

    return WillPopScope(
      onWillPop: () => _backPressed(_yourKey),
      child: Scaffold(
        body: Navigator(
          key: _yourKey
          onGenerateRoute: _yourMaterialPageRouteLogic,
        ),
        bottomNavigationBar: CustomNavigationBar(navBarOnTapCallback),
      ),
    );

Your key can be instatiated like this

    GlobalKey<NavigatorState> _yourKey = GlobalKey<NavigatorState>();

The _backPressed method will receive any backPressed you do on your device. By definition it returns true, and we don't always want to pop.

We've added a key to the navigator, and now it will be used to understand whether the new Navigator has anything in the stack in order to be popped (ie if it 'canPop').

Future<bool> _backPressed(GlobalKey<NavigatorState> _yourKey) async {
  //Checks if current Navigator still has screens on the stack.
  if (_yourKey.currentState.canPop()) {
    // 'maybePop' method handles the decision of 'pop' to another WillPopScope if they exist. 
    //If no other WillPopScope exists, it returns true
    _yourKey.currentState.maybePop();
    return Future<bool>.value(false);
  }
//if nothing remains in the stack, it simply pops
return Future<bool>.value(true);

Next you just need to add the WillPopScope anywhere inside your Navigator. Its onWillPop will be called with the logic you want. Don't forget to return true or false depending on whether you want to pop it or not :)

Here is a example of my onWillPop method (_popCamera) in a WillPopScope widget, which is placed inside my Navigator widget tree. In this example I've added a dialog when the user presses the back button in the Camera Widget:

  static Future<bool> _popCamera(BuildContext context) {
    debugPrint("_popCamera");
    showDialog(
        context: context,
        builder: (_) => ExitCameraDialog(),
        barrierDismissible: true);

    return Future.value(false);
  }

class ExitCameraDialog extends StatelessWidget {
  const ExitCameraDialog({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Leaving camera forever'),
      content:
      Text('Are you S-U-R-E, S-I-R?'),
      actions: <Widget>[
        FlatButton(
          child: Text('no'),
          onPressed: Navigator.of(context).pop,
        ),
        FlatButton(
          //yes button
          child: Text('yes'),
          onPressed: () {
            Navigator.of(context).pop();
            _yourKey.currentState.pop();
          },
        ),
      ],
    );
  }

Hope it's clear!

like image 32
Tiago Santos Avatar answered Oct 13 '22 14:10

Tiago Santos