In Flutter, the screens or pages presented to you in a mobile app are called routes. Behind the scenes, while you navigate through the app going from screen to screen, a ‘stack of routes’ is being managed by the StatefulWidget, Navigator. There is an actual class called, Navigator, involved in all this. With this article, we’ll introduce ourselves to this class as well as take a tentative look at Flutter’s navigation system. Subsequent articles on the subject will follow.
I Like Screenshots. Click For Gists.
As always, I prefer using screenshots in my articles over gists to show concepts rather than just show code. I find them easier to work with frankly. However, you can click or tap on these screenshots to see the code in a gist or in Github. Further, it’s better to read this article about mobile development on your computer than on your phone. Besides, we program mostly on our computers — not on our phones. Not yet anyway.
No Moving Pictures, No Social Media
There may be gif files in this article demonstrating aspects of the topic at hand. However, it’s said viewing such gif files is not possible when reading this article on platforms like Instagram, Facebook, etc. They may come out as static pictures or simply blank placeholder boxes. Please, be aware of this and maybe read this article on medium.com
It’s not quite a ‘stack of routes’ by the way. The class object, Navigator, works directly with a stack of StatefulWidgets called, Overlay. Well, that’s not quite right. More often than not, it’s a stack of StatelessWidgets called, ModalBarrier, each ‘wrapped’ (or passed into) an Overlay widget.
Each Overlay also takes in a child widget (an app’s screen) and this child widget then ‘visually floats’ on top of the Overlay. Again, these Overlay widgets are literally collected one after the other in the order they were created as you progress through your app using the function,
Navigator.push() — forming what is described as the ‘stack of screens.’ Again, a ModalBarrier widget is used by each Overlay to prevent any interaction with the Overlays ‘stacked’ below or more specifically with the child widget enveloped by the previous Overlay widget. Got it so far?
In fact, that’s not quite true either. In the build() function of the StatelessWidget, ModalBarrier, you’ll find further widgets are involved in disallowing interaction with those widgets stacked underneath the current Overlay (See the screenshot below). These further widgets hide in particular the ‘semantic information’ of the previous widgets that’s traditionally provided to any and all accessibility tools. Anyway, you get the message. It’s a stack of Routes/Overlays/ModalBarriers managed by the Navigator widget.
By the way, a common instance where you would actually see the ModalBarrier widget at work is when a typical Flutter dialog box is displayed on the screen. It’s then you’ll notice the previous screen/page/route behind the dialog box has darkened in brightness. That’s the doing of the ModalBarrier object found in the topmost Overlay widget. The ModalBarrier is just as evident when a Drawer widget is opened in your app — again the background is darkened and not readily accessible.
Which Route Depends On Which Platform
The ‘type of routes’ (screens) used by your Flutter app comes down to whether you’re running your app using the ‘Material design’ or the ‘Cupertino design’ (or either if your app picks one depending on the platform it’s running on). In other words, how the routes appear onto the screen and then exit the screen (the transition of your pages) depends on whether your app uses the MaterialApp widget (commonly seen in examples) or uses the CupertinoApp widget which emulates the ‘look and feel’ of a traditional iOS app.
For example, when writing a Flutter app, and you’re building your home screen, know that if your app is starting with the MaterialApp widget, your home screen will be ‘wrapped around’ the object, MaterialPageRoute. You can readily see this in the screenshot below on the left-hand side. Using the MaterialApp widget, the anonymous function assigned to its named-parameter, pageRouteBuilder, produces a MaterialPageRoute widget. On the right-hand side, if your app is using the CupertinoApp widget, the route widget used will be, CupertinoPageRoute. Pretty straight-forward, no?
Further note, as you see in each screenshot above, both ‘interface designs’ in Flutter use the WidgetsApp widget. It serves as the foundation of your app. It is that widget that’s essentially the base of the widget tree. From its own class documentation, it states, ‘Creates a widget that wraps a number of widgets that are commonly required for an application.’
A Simple Route
To help demonstrate this article’s topic, I’m using the example app used in the Cookbook exercise, Navigate to a new screen and back. Ok, that’s not quite right, I’m using a combination of two example apps. A second example app from the Cookbook exercise called, Return data from a screen, is incorporated with the first so as to more fully explore Flutter’s approach in navigating between screens — I mean, routes. The example app is available as a gist and is called, nativgate_two_routes.dart.
A Route Home
Let’s now take a look at the main() function in our example app. There’s a screenshot of the function below — accompanied by a gif demonstrating the running app. Right off the hop, we see the MaterialApp() widget is being used, and so we know any and all routes (screens) used are going to follow the Material design specifications in appearance and behavior.
Highlighted below is the parameter, home, and it’s assigned the StatelessWidget, FirstRoute. Since the MaterialApp widget is being used, we know then a MaterialPageRoute widget will be ‘wrapped around’ (i.e. taken in as a parameter) the widget, FirstRoute. Let’s walk through the code and show this.
Below is a screenshot of the State object, _WidgetsAppState. It’s a screenshot of its build() function, and it’s there where the class object, Navigator, is first instantiated. Further, as it pertains to our particular example app, the private function, _onGenerateRoute(), is passed on to the Navigator object. It’s yet another function involved in generating the route object for the ‘Home Screen.’
Navigate Your Route
And so, as we continue on, we see the State object, NavigatorState, determines in its initState() function what is to be the ‘name of the initial route’ for the Flutter app. It does this by calling the onGenerateInitialRoutes() function found in its StatefulWidget, Navigator, and passing in, by default if not explicitly specified, the backslash, ‘/’, as the ‘name’ for the initial route (In this case, it’ll be that ‘Home Screen’). By the way, it is that List object, _history, in the screenshot below that will contain the so-called ‘stack of routes’ while you're moving from screen to screen:
List _history = <_RouteEntry>;
Next, in most instances, the onGenerateInitialRoutes() function will then call the private function, _routeNamed. It is in the _routeNamed() function (see below) where we first see a reference to a Route object. Note, a RouteSettings object is instantiated there and passed on to yet another function, onGenerateRoute. It is a RouteSetting object that is the one lone parameter passed to a Route class constructor when it’s about to be instantiated. We’re getting close now.
For this particular app, calling the function, onGenerateRoute, takes us back to the WidgetsApp State object, and its _onGenerateRoute() function. This function is displayed below, and there, you see the named-parameter, home, is passed along to the local variable, pageContentBuilder. Finally, the anonymous function we first encountered being assigned to the named-parameter, pageRouteBuilder, takes in that local variable producing the Route object, MaterialPageRoute. Whew! There’s a lot to this Flutter framework ain’t it? We haven’t even looked at the class, Route, yet. That’s next.
The Road to Route
5. pageRouteBuilder: <anonymous function>
MaterialPageRoute<T>(settings: settings, builder: builder);
You’ve Many Routes
Again, every ‘screen’ in a Flutter app, involves an object of the class, Route. Of course, as you now know, it involves a number of other classes, but we’ll now look at the Route class. It’s an abstract class. As you see below, it is merely the base class of a long hierarchy of abstract classes that extend on and on right down to the ‘type of route’ offer by the MaterialApp widget and the CupertinoApp widget. Each of their class declarations is listed below, and you can see each ‘intermediate’ abstract class has a specific role to play in maintaining this ‘stack of screens.’
We’ll take a look at each of those intermediate classes and the roles they play in subsequent articles. Allow me to finish this article by examining the example app a little more, and briefly see what’s involved in ‘returning’ to a previous screen (route). In this app, returning brings back a particular value which is displayed on the app’s SnackBar — we’ll take a look at that now.
The Route To NoWhere
When incorporating the second example, Return data from a screen, from the Flutter’s Cookbook exercises, I noticed something was missing. Do you see the difference between its code and the example app’s code underneath?
Yes, it's missing the ‘data type’ for the return value. I mean, being such a simple example, it’s no biggy, but it is good practice to specify it — especially if you’re actually waiting for the Future value to be returned. In our example app, it’s return value is of the data type, String. You’ll find many of the static functions in the class, Navigator, allow for the data type argument.
Back To Null
There’s a problem with the example app. I’ve mixed two example apps together you see. I mixed one which didn’t wait for a returned value with one that did, and so there will be a little bit of a discrepancy found in its execution. Pressing the ‘back button’ will result in the SnackBar widget displaying the word, null. That’s because, as you see in the AppBar class below, the ‘back button’ or ‘back arrow’ doesn’t accommodate a returned value at all.
No Back Up
The StatelessWidget, BackButton, when pressed merely calls the static function, maybePop, without specifying a returned result.
You can see in the screenshot of that maybePop() function below, it does allow for it with the generic type, T, but it’s simply not done when pressing the back button, and so we get null for a return value.
Looking inside that static function displayed above, you see another static function,
Navigator.of().It’s commonly used to get a reference to the Navigator’s State object counterpart, NavigatorState, passing in the optional return value of the generic type, T. By the way, when it does return an instance of the State object, it’s from the Navigator widget we saw instantiated way back in the WidgetsAppState’s build() function. Remember?
Below is a screenshot of that State object’s maybePop() function. Much of the Flutter framework when appropriate, use the maybePop() function and not the pop() function which just blindly ‘pops’ the topmost route entry. As you see below, the maybePop() function first tests the topmost route entry (represented by the variable, lastEntry) is the right entry to ‘pop’ and not only performs the operation returning a Future value if available, but the function itself returns a boolean True if successful. Lastly, notice the variable, lastEntry, comes from the List object, _history, — again representing the ‘stack of routes.’
At Times, Test For Null
By the way, with regards to this particular example app, the workaround I would have chosen would be to ‘test for null’ and ignore a null returned value if the user presses the ‘back button’ or the ‘close button’ (with parameter fullscreenDialog: true). See below.
Mix Or Match
By the way, it is possible to switch out the second MaterialPageRoute used in this simple example app with a CupertinoPageRoute. Note, the transition between screens now involves the second screen moving in and out from the right side and no longer from the bottom.
This article was a little erratic, I know, but I decided to present the Flutter’s navigation system in this fashion in the hope you’d appreciate the mark of complexity that’s involved in what’s seemingly a fundamental operation — going from window to window. I mean, screen to screen. I mean, route. You know what I mean!! We’ll continue our journey in future articles.