Flutter — navigating off the charts

Swav Kulinski
4 min readMay 29, 2018

--

Flutter comes equipped with the Navigator class which allows defining Routes for an application flow. A Route determines the:

  • Widget you are going to show
  • Animations you are going to use when showing and hiding the Widget
  • State of animations

Very often developers create StatefulWidgets to do just that, completely forgetting about the Navigator and all advantages it brings to the app.

It’s simple, isn’t it?

It is very straightforward to hack together something like a Navigator. The example below shows a StatefulWidget which can replace its contents based on the state. After reading this snippet please also read the comment below why this is a hack.

Note: Do not do that! Above example is a very dirty hack, and soon you will learn why.

In the snippet above I write the index of the current page to the state of the widget. Clicking the button changes the state, so on each new state, a new page is displayed. However, if you have an Android device, there is a nasty surprise waiting for you. Please, press the back button, and here it is:

Yes, I didn’t keep the back stack of the navigation, and the application was sent to the background.

Navigator to the rescue

This is where the Navigator class comes in handy. Let’s upgrade our sample.

In the sample above I have a map of the factory methods called pages.

{"/": () => MaterialPageRoutes(....), ...}

You may ask why I bother with functions, why I don’t just store route instances in the map? I can’t do it. Look at the asserts inside the Navigator.push() method.

@optionalTypeArgs
Future<T> push<T extends Object>(Route<T> route) {
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }());
assert(route != null);
assert(route._navigator == null);
...
}

The line highlighted in bold means that if I have used this MaterialPageRoute instance already I can’t do it again. This is why instead of storing MaterialPageRoutes I am storing functions that return an object of that class.

Pushing

I can push a new route using the line below.

onPressed: () => Navigator.of(context).pushNamed(nextRouteName)

Each tap on the button adds a new route to the back stack, and the new page appears on the screen.

Now let’s run it. You probably have noticed already that this app behaves somehow differently. It plays an animation of new page sliding into the screen. It is different on iOS and Android. If you run on Android, you may also notice that the back button on the device now navigates back through the pages before closing the app.

We may think that this solves all navigation struggles, but that’s not true. What Navigator.push() or Navigator.pushNamed() does under the hood is traverse the tree up in search of a MaterialApp widget and scan it for possible routes. If it can’t find a matching one, it will throw an error.

What to do if my routes sit on a sibling branch?

Consider following layout structure. What if we want to navigate the content of MaterialApp from BottomNavigationBar?

You may be asking why I want to implement such a structure? Let’s say I want the content of the page to change as I tap the buttons on the bottom bar. But at the same time, I don’t want to redraw the bottom bar at all. It’s a legit scenario, right?

Here is the code which builds the structure presented above.

As before we define our routes in the map and we use them in onGenerateRoute in line 37 and in line 47 where we invoke Navigator.pushNamed(). However, this navigation fails with:

I/flutter (25356): Another exception was thrown: Navigator operation requested with a context that does not include a Navigator.

Our culprit is here.

onTap: (routeIndex) =>              
Navigator.of(context)
.pushNamed(pagesRouteFactories.keys.toList()[routeIndex]),

The call above will ask Navigator to traverse the tree up in search of routes that match the route name. It can’t find it, because our route sits on the sibling branch. We need to somehow jump from one branch to another.

MaterialApp.navigatorKey

I have examined the MaterialApp class, and I have found an interesting member called navigatorKey of type GlobalKey<NavigatorState>. We can create it outside of the widget, pass it as a parameter to MaterialApp. Then we can use the reference to this object any time we want to manipulate the navigation.

The first step is to add an instance of GlobalKey<NavigatorState> to our top widget:

class TopLevelWidget extends StatelessWidget { final navigatorKey = GlobalKey<NavigatorState>(); ...
}

Then we have to assign it to the MaterialApp widget:

Widget _buildBody() =>
MaterialApp(
navigatorKey: navigatorKey,
onGenerateRoute: (route) => pagesRouteFactories[route.name]()
);

The last bit is to replace the navigator call, so it uses navigatorKey.

onTap: (routeName) =>
navigatorKey.currentState.pushNamed(pagesRouteFactories.keys.toList()[routeName]),

Now entire application in full.

--

--

Swav Kulinski

Computer enthusiast since 80s, Android developer since Eclair, now Flutter enthusiast. https://gitlab.com/swav-kulinski