Skip to content

Support for vetoing an attempt to pop the current route#7488

Merged
HansMuller merged 4 commits intoflutter:masterfrom
HansMuller:willPop
Jan 18, 2017
Merged

Support for vetoing an attempt to pop the current route#7488
HansMuller merged 4 commits intoflutter:masterfrom
HansMuller:willPop

Conversation

@HansMuller
Copy link
Contributor

@HansMuller HansMuller commented Jan 13, 2017

It's often useful for a Form widget to warn or even block the user if they attempt to back out of an invalid or unsaved modal form.

The Form willPop callback is called before a Navigator.pop() triggered by the Scaffold's back button or Android's system back button. The value of the callback is a Future<bool>. The enclosing modal route will only be popped if willPop returns true. A simple application of willPop would be to show an alert that asks the user to confirm:

new Form(
  onWillPop: () {
    return showDialog(
      context: context,
      child: new AlertDialog(
        title: new Text('This form has errors'),
        content: new Text('Really leave this form?'),
        actions: <Widget> [
          new FlatButton(
            child: new Text('YES'),
            onPressed: () { Navigator.of(context).pop(true); },
          ),
          new FlatButton(
            child: new Text('NO'),
            onPressed: () { Navigator.of(context).pop(false); },
          ),
        ],
      ),
    );
  },
  // ...
)

In this example showDialog returns true or false depending on the Navigator.pop() value it was dismissed with.

The Route class now has a willPop method with just returns Future<bool>.value(true) by default.

The ModalRoute class overrides willPop and delegates to its _ModalScope. This makes it possible to define willPop from a stateful descendant of a ModalRoute. Typically apps don't define ModalRoute subclasses directly, they just use higher level primitives that create ModalRoutes, like showDialog() or MaterialPageRoute. The ModalRoute scopedWillPopCallback makes it possible for a ModalRoute's stateful descendant, like showDialog's child, to define the value of willPop.

@override
void dependenciesChanged() {
 super.dependenciesChanged();
 final ModalRoute<Null> route = ModalRoute.of(context);
 if (route.isCurrent)
   route.scopedWillPopCallback = askTheUserIfTheyAreSure;
}

Navigator.willPop() now delegates to the current route's willPop method. The Scaffold's back button and the binding for the Android system back button now check Navigator.willPop() before actually popping the current route.

Fixes #6331

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe change these to pumpUntilNoTransientCallbacks ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's WAY better. I didn't realize that pumpUntil... existed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to check mounted here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the way I understand didPopRoute's role in all of this.

If didPopRoute() returns true it just means that the pop request has been dealt with. In this case willPop() defeats the request, i.e. it's been dealt with, even if the route was disposed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there are two forms in a route? Also, don't we need to unregister the callback on dispose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right (Ian had brought this up as well, but I thought I could avoid it). A Route can have an arbitrary number of Form descendants. So I'll need to manage a list of callbacks, rather than just one.

@eseidelGoogle
Copy link
Contributor

What's the expected interaction on iOS? Does this kind of interaction only happen for full-screen dialogs on ios? Presumably this doesn't occur when someone uses the backgesture on ios? :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also need to check mounted here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the route check the isCurrent thing?
I think this can all be simplified to just return route.willPop(), if the route verifies isCurrent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need to check mounted here, that's the caller's problem.

@Hixie
Copy link
Contributor

Hixie commented Jan 13, 2017

The My expectation is was that back gesture pages not use this, and back gesture therefore doesn't go through this path.

edit: see later comment

@HansMuller
Copy link
Contributor Author

@eseidelGoogle Yes. That's what the change in page.dart is for.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're returning a Future<Future> here. You should just return true, and async will wrap it for you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably need to take a local copy of _observers here or otherwise handle what happens while this now long-running API gets fiddled with while in flight. We used to have the rule that didPopRoute couldn't change the observers but that no longer works since it's entirely async and minutes could pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dynamic, not Null. You don't know what the route's return type will be here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dynamic not Null

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the isCurrent check?

Copy link
Contributor

@Hixie Hixie Jan 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future<bool> willPop async => true;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto above

Copy link
Contributor

@Hixie Hixie Jan 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/,/, and/ and s/;/./

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// not ///

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the route callback mechanism should be a list of callbacks, not a single callback

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this limitation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure if the _ModalScope would always exist, even when the route wasn't current. Maybe it always does; I'll remove the isCurrent checks if that's so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _ModalScope is just another widget. If you're in the tree in a Navigator in a ModalRoute, then you must have a _ModalScope because the ModalRoute builds it.

@Hixie
Copy link
Contributor

Hixie commented Jan 13, 2017

In reading the patch I found that the way the patch does the back gesture thing is it just disallows the back gesture if you have any callbacks registered. That seems fine too.

Copy link
Contributor

@Hixie Hixie Jan 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't this return a Future<Null>?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh never mind, I see.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be showDialog/*<bool>*/(...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it returns a Future whose value is the bool passed to the Navgator.pop() that dismisses the Alert dialog.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. So it should be showDialog/*<bool>*/(... (rather than just showDialog( like here)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be careful about using pumpUntilNoTransientCallbacks. It can hide bugs. In particular here I think it would be better to just have the two pumps, because then we're clear about how many frames we're expecting. Why would we be getting more?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scaffold's back button callback now awaits the willPop() method before actually calling Navigator.pop(). That means that tests that back out of a route by tapping the back button now need to pump three times to be consistent with the implementation. I'll add the explicit pumps (back) but binding the test to the implementation in this way doesn't seem that useful to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, do we fully understand why we're getting more than two?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be mentioned in the docs, too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand this comment. There's no test anywhere that it's the same modal route...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens in the gallery. Each time the "This form has errors" warning dialog is pushed or popped the underlying Form's dependenciesChanged() method runs. We don't want to keep adding the same callback to the route.

I still need to write a test that simulates this scenario.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does it matter if it's current?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't matter. I'd meant to remove that, thanks for repointing it out.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't you need to check if oldConfig.onWillPop was null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this would be better starting off as null, since it's a rare case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had it that way originally. Doing so makes all of the other references to willPopCallbacks messier; I don't think there's much of a win here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. I didn't realise it was used from outside this class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert that it's not that const that you set in the dispose method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an assert(mounted)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably warn that the callback might be called long after the route was unmounted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never mind, i see this is the private api

Copy link
Contributor

@Hixie Hixie Jan 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably warn that this callback might get called long after the route and the widget tree is disposed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the doc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's null you return true?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is intentional, please add a comment as to why...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was intentional and consistent and unnecessary. I've removed all of them.

@Hixie
Copy link
Contributor

Hixie commented Jan 18, 2017

LGTM with the one remaining fix (mentioning the effect of registering a willPop callback on the iOS back gesture).

@HansMuller HansMuller merged commit 0ce9917 into flutter:master Jan 18, 2017
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 14, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support warning users before leaving route containing an unfinished form

5 participants