Flutter: Push, Pop, Push

Building UI in Flutter is pretty simple with all the widgets that the framework provides, part of which I covered in my last article. But we can’t just have a beautiful application that does nothing functional. We will be required to move around the application or send data back and forth between screens. In Flutter, navigation from one screen to another is possible because of Navigators, a simple widget that maintains a stack of Routes, or in simpler terms, a history of visited screens/pages.

You will find plenty of articles that tell you how to push to a new screen or pop from the current screen, but this article is a little more than that. This would primarily focus on most of the Navigator methods and describe a use-case for each method.

Before we begin…

You mentioned about Routes somewhere, what was that?
Routes is an abstraction for a screen or a page of an app. For example, '/home' will take you to HomeScreen or '/login' will take you to LoginScreen. '/' will be your initial route. This might sound so much similar to Routing in REST API development. So '/' might act like a root.

This is how you would declare your routes in your Flutter application.

new MaterialApp(
home: new Screen1(),
routes: <String, WidgetBuilder> {
'/screen1': (BuildContext context) => new Screen1(),
'/screen2' : (BuildContext context) => new Screen2(),
'/screen3' : (BuildContext context) => new Screen3(),
'/screen4' : (BuildContext context) => new Screen4()
},
)

Screen1(), Screen2(), etc are the names of the classes for each screen.


Push, push, push.

If you have any knowledge in Data Structures, then you know about Stacks. If you have even basic knowledge of stacks, then you know about push and pop.

If you don’t, pushing is adding element to the top of a stack of elements and popping is removing the top element from the same stack.

So in case of Flutter, when we navigate to another screen, we use the push methods and Navigator widget adds the new screen onto the top of the stack. Naturally, the pop methods would remove that screen from the stack.

So lets move to the codebase of our sample project and let’s see how we can move from Screen 1 to Screen 2. You can experiment with the methods by running the sample app.

new RaisedButton(
onPressed:(){
Navigator.of(context).pushNamed('/screen2');
},
child: new Text("Push to Screen 2"),
),

That was short. 
 
That was indeed. With the help of pushNamed methods, we can navigate to any screen whose route is defined in main.dart. We call them namedRoute for reference. The use-case of this method is pretty straightforward. To simply navigate.

Stack after pushing Screen2

Pop it

Now when we want to get rid of the last visited screen, which is Screen2 in this case, we would need to pop Routes from the Navigator’s stack using the pop methods.

Navigator.of(context).pop();

Remember, this line of code goes inside your onPressed method.

Stack after Screen 2 popped

When using Scaffold, it usually isn’t necessary to explicitly pop the route, because the Scaffold automatically adds a ‘back’ button to its AppBar, which would call Navigator.pop() on pressed. Even in Android, pressing the device back button would do the same. But nevertheless, you might need this method for other usecases such as popping an AlertDialog when user clicks on Cancel button.

Why pop instead of pushing back to the previous screen?
Imagine you have a Hotel Booking app that lists the hotels in your desired location. Clicking on any list item will take you to a screen that has more details about the hotel. You choose one, and you hate the hotel and want to go back to the list. If you push back to the HotelListScreen, you will be keeping your DetailsScreen onto your stack too. So pressing on the back button would take you back to the DetailsScreen. So confusing!

You should try it out instead. Run the sample app, notice the appBar on your Screen1 , it doesn’t have any back button, because it is the initial Route (or home screen), now press Push to Screen 2 and instead of back button, press the Push to Screen1 instead of Pop and now notice the appBar on Screen1. Pressing on the newly appeared back button will take you back to Screen2 and you don’t want that in such cases.

This will be the Stack on such a situation

maybePop

Now what if you are on the initial Route and somebody mistakenly tried to pop this screen. Popping the only screen on the stack would close your app, because then it has no route to display. You definitely don’t want your user to have such an unexpected user experience. That’s where maybePop() comes into picture. So it’s like, pop only if you can. Try it out, click on the maybePop Button on Screen1 and it will do nothing. Because there is nothing to pop. Now try the same on Screen3 and it will pop the screen. Because it can.

canPop

It’s amazing that we can do this, but how can I know if this is the initial route? It would be nice if I could display some alert to user in such cases. 
Great question, just call this canPop() method and it would return true if this route can be popped and false if it’s not possible.

Try both these methods canPop and maybePop on Screen1 and Screen3 and see the difference. The print values for canPop will display in your console tab of your IDE.


Push a little harder

Let’s come back to more push methods. Now we are getting deeper. We will now talk about replacing a route with a new route. We have two methods that can do this — pushReplacementNamed and popAndPushNamed.

Navigator.of(context).pushReplacementNamed('/screen4');
//and
Navigator.popAndPushNamed(context, '/screen4');
Stack before and after

Try to experiment with both in the sample app’s Screen3. And notice the exit and enter animation in each case. pushReplacementNamed will execute the enter animation and popAndPushNamed will execute the exit animation. We can use this for the following possible use-cases.

Use-case : pushReplacementNamed

When the user has logged in successfully, and are now on the DashboardScreen perhaps, then you don’t want the user to go back to LoginScreen in any case. So login route should be completely replaced by the dashboard route. Another example would be going to HomeScreen from SplashScreen. It should only display once and the user should not be able to go back to it from HomeScreen again. In such cases, since we are going to a completely new screen, we may want to use this method for its enter animation property.

Use-case: popAndPushNamed

Suppose, you are building a Shopping app that displays a list of the products in its ProductsListScreen and user can apply filters in the FiltersScreen. When the user clicks on the Apply Changes button, the FiltersScreen should pop and push back to the ProductsListScreen with the new filter values. Here the exit animation property of popAndPushNamed would look more appropriate.


Until the end…

We are almost at the end of the article. Well, almost. 
In this section, we will cover the following three methods pushNamedAndRemoveUntil and popUntil.

Use-case: pushNamedAndRemoveUntil

So basically you building a Facebook/Instagram like application, where user logs in, scrolls through their feed, stalks through different profiles, and when done, wants to log out of the app. After logging out, you can’t just simply push a HomeScreen (or any screen that needs to be displayed after logging out) in such cases. You want to remove all routes in the stack so that user cannot go back to the previous routes after they have logged out.

Navigator.of(context).pushNamedAndRemoveUntil('/screen4', (Route<dynamic> route) => false);

Here (Route<dynamic> route) => false will make sure that all routes before the pushed route be removed.

Logging out removes all routes and takes user back to LoginScreen

Now instead of removing all routes before the pushed routes, we can only remove certain number of routes. Let’s take another Shopping app example! Or basically any app that requires payment transaction.

So in these apps, once a user has completed the payment transaction, all the transaction or cart related screens should be removed from the stack, and the user should be taken to the PaymentConfirmationScreen. Clicking on the back button should take them back to the ProductsListScreen or HomeScreen only.

Navigator.of(context).pushNamedAndRemoveUntil('/screen4', ModalRoute.withName('/screen1'));

So according to the code snippet, we push Screen4 and remove all routes until Screen1, so our stack would look like this.

Before and after

Use-case: popUntil

Imagine you are building a Google Forms like application or an app that lets you fill and organize Google forms. Now some user might have to fill a long 3-part form, which might be displayed in 3 sequential screens in a mobile application. Now on the 3rd part of the form, user decides to cancel filling the form. User clicks on Cancel and all the previous form related screens should be popped and user should be taken back to the HomeScreen or DashboardScreen thereby losing all the form related data (which is what we want in such cases). We won’t be pushing anything new here, just going back to a previous route.

All the Form related screens are popped
Navigator.popUntil(context, ModalRoute.withName('/screen2'));

Where is the data?

In most of the examples above, I am just pushing a new route without sending any data but such a scenario is highly unlikely in a real application. To send data, we would be using Navigator to push a new MaterialPageRoute onto the stack with our data, (here it’s userName)

String userName = "John Doe";
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) =>
new Screen5(userName)));

To retrieve the values in Screen5 , we would just add a parameterized constructor in Screen5 like this:

class Screen5 extends StatelessWidget {

final String userName;
Screen5(this.userName);

@override
Widget build(BuildContext context) {
print(userName)
...
}
}

This means that you can not only use MaterialPageRoute for push method, but also for pushReplacement, pushAndPopUntil, etc. Basically drop the named keyword from the above methods we described, and the first parameter will now take a MaterialPageRoute instead of a String of namedRoute.

Give me some data back, man

You might also want to return data from a new screen. Like suppose you are building a Alarm app, and to set a new tone for your alarm, you would show a Dialog with a list of audio tone options. Obviously you would need the selected item data once that Dialog box has been popped. It can be achieved like this:

new RaisedButton(onPressed: ()async{
String value = await Navigator.push(context, new MaterialPageRoute<String>(
builder: (BuildContext context) {
return new Center(
child: new GestureDetector(
child: new Text('OK'),
onTap: () { Navigator.pop(context, "Audio1"); }
),
);
}
)
);
print(value);

},
child: new Text("Return"),)

Try it out in Screen4 and check the console for the print values.

Also to note : When a route is used to return a value, the route’s type parameter should match the type of pop’s result. Here we need a String data back so we’ve used MaterialPageRoute<String>. It’s alright to not specify the type too.

Wow, that was a lot of information

It was indeed, and I could have just described the methods and their implementation simply, but since there are so many Navigator methods, I wanted to explain them using scenarios taken from real world applications. I hope this helped some of you broaden your Navigator horizon.

Read my other articles on Flutter


I got something wrong? Mention it in the comments. I would love to improve.
If you learnt even a thing or two, clap your hands 👏 as many times as you can to show your support!
Feeling too generous? Buy me a cupcake
Hello World, I am Pooja Bhaumik. A creative developer and a logical designer. You can find me on Linkedin or stalk me on GitHub or maybe follow me on Twitter? If that’s too social for you, just drop a mail to pbhaumik26@gmail.com if you wish to talk tech with me.
Have a nice fluttery day!