From Flutter in Action by Eric Windmill

Learn a bit about using Flutter by learning about routing in a Farmer’s Market application.


Take 37% off Flutter in Action. Just enter fccwindmill into the discount code box at checkout at manning.com.


The Farmer’s Market app

I live in Portland, Oregon, where people love Farmer’s Markets. I mean deeply love them. In an unnatural way. I thought I’d get rich by breaking into that market. This is the app I made for people to buy veggies and other treats from farmers:



The structure of the app isn’t too complicated; it’s four pages. Our job in this article is to wire-up that menu with some routes, and create a few on-the-fly routes. Then, we’re going to make it fancy with some page transition animations.

The interesting thing about the routes in this app, in my biased opinion, is that all the pages share the same structural elements. The app bar, the cart icon, the menu, some of the

Functionality and the scaffold are all written once, and I pass in the configuration based on the route. This is possible for two reasons: the way we compose UI in Flutter, and the fact that the Navigator’s a widget that doesn’t have to be top level.

 

The app source code

In the git repository, these are the relevant files for the app:

Listing 1. Important files the e-commerce app

 
 lib ├── blocs 
 │   ├── app_bloc.dart
 │   ├── cart_bloc.dart
 │   ├── catalog_bloc.dart
 │   └── user_bloc.dart
 ├── menu 
 │   └── app_menu_drawer.dart
 ├── page 
 │   ├── base
 │   │   ├── page_background_image.dart
 │   │   ├── page_base.dart
 │   │   └── page_container.dart
 │   ├── cart_page.dart
 │   ├── catalog_page.dart
 │   ├── product_detail_page.dart
 │   └── user_settings_page.dart
 ├── utils 
 │   ├── material_route_transition.dart
 │   └── styles.dart
 ├── widget 
 │   ├── add_to_cart_bottom_sheet.dart
 │   ├── appbar_cart_icon.dart
 │   ├── catalog.dart
 │   └── product_detail_card.dart
 ├── app.dart 
 └── main.dart
 

This is where the logic lives.

We’ll cover the menu in-depth in this article.

The pages are (mostly) already built in this article, but we we’ll wire a lot of it up in this article.

The material_route_transition.dart file in this directory is the most fun part of the article. It’s a custom animation from one page to another.

Most of these files have to do with “on the fly” navigating.

The root widget of the project, which is where the next section starts.

 

Declarative Routing and Named Routes

If you’ve built web apps or mobile apps on nearly any other platform, you’ve likely dealt with declarative routing. On the other application platforms that I’ve used, (such as Ruby on Rails, Django, front-end libraries of the not- distant past), routes are defined in their own “routes” file, and what you declare is what you get. In AngularDart, your routes page may look like this:

Listing 2. AngularDart Router route definitions

 
 static final routes = [     new RouteDefinition(
         routePath: new RoutePath(path: '/user/1'); 
         component: main.AppMainComponentNgFactory), 
     new RouteDefinition(
         routePath:  new RoutePath(path: '/404');
         component: page_not_found.PageNotFoundComponentNgFactory)
     //... etc
   ];
 

The “name” of the route.

Which component should render at that route.

Understanding Angular code isn’t important, it’s an example of up-front route declarations written in Dart. The point is that you tell your app explicitly what routes you want to exist, and which views they should route to. Each RouteDefinition has a path and a component (which is probably a page). This is generally done at the top level of an app. Pretty standard stuff here.

Flutter supports this. While the routes and pages are still built as the app is running, the mental-model you can approach this with is that they’re static.

Mobile apps often support tens of pages, and it’s perhaps easier to reason about if you define them once and then reference them by name, rather than creating unnamed routes all over the app.

Flutter routes follow the path convention of all programing, such as “/users/1/inbox” or “/login”. And as you’d expect, the route of the home page of your app is “/”.

 

Declaring routes

Using named routes requires two parts. The first is defining the routes. In the ecommerce app you’re building in this article, the named routes are set up in the lib/main.dart file. If you navigate to that file, you’ll see a MaterialApp widget with the routes established, and in utils/e_commerce_routes.dart file you’ll see the static variables with the route names. (This is to safely use routes without fearing typos in the strings.)

Listing 3. Define Routes in the MaterialApp widget

 
 // e_commerce/lib/main.dart -- line ~ 51 // ... return MaterialApp(   debugShowCheckedModeBanner: false,
   theme: _theme,
   routes: { 
     ECommerceRoutes.catalogPage: (context) =>
         PageContainer(pageType: PageType.Catalog),
     ECommerceRoutes.cartPage: (context) =>
         PageContainer(pageType: PageType.Cart),
     ECommerceRoutes.userSettingsPage: (context) =>
         PageContainer(pageType: PageType.Settings),
     ECommerceRoutes.addProductFormPage: (context) =>
         PageContainer(pageType: PageType.AddProductForm),
   },
   navigatorObservers: [routeObserver],
 );
 // e_commerce/lib/utis/e_commerce_routes.dart class ECommerceRoutes { ❷   static final catalogPage = '/';
   static final cartPage = '/cart';
   static final userSettingsPage = '/settings';
   static final cartItemDetailPage = '/itemDetail';   static final addProductFormPage = '/addProduct';
 }
 

This is where you define the rest of your named routes. Named routes are defined in a

Map, where the key is the name of the route (“/”), and the value’s a function which returns a widget.

The EcommerceRoutes class maps to these routes for constant-variable safety.

 

Navigating to Named Routes

Navigating to named routes is as easy as using the Navigator.pushNamed method. The pushNamed method requires a BuildContext and a route name, and you can use it anywhere that you have access to your BuildContext.

 
var value = await Navigator.pushNamed(context, "/cart");
 

Pushing and popping routes is the bread and butter of routing in Flutter. Recall that the Navigator lays its children (the pages) out in a ‘stack’ nature. The stack operates on a “last in, first out” principal, as stacks do in computer science. If you’re looking at the home page of your app, and you navigate to a new page, you “push” that new page on top of the stack (and on top of the home page). The top item on the stack is what you see on the screen. If you pushed another route, we’ll call it page three, and wanted to get back to the home page, you’d have to “pop” twice.


Figure 1. Flutter’s navigator is a stack-like structure


The Navigator class has a bunch of helpful methods to manage the stack. This is only a handful of the methods I find myself using:

  • pop
  • popUntil
  • canPop
  • push
  • pushNamed
  • popAndPushNamed
  • replace
  • pushAndRemoveUntil

One important note about pushing named routes is that they return a Future. If you’re not familiar with the await keyword above, it’s used mark expressions that return an asynchronous value. We’re not going to get into async Dart quite yet, but, the quick version is when you call Navigator.pushNamed, because it immediately returns a Future object, which says “Hey, I don’t have a value you for you yet, but I will as soon as this process finishes”. In the specific context of routing, this means “As soon as they navigate back to here from the page which they’re on now, I’ll give you whatever value they pass back from that page.”

In the e-commerce project repository, we can find an example of using Navigator.pushNamed in the AppBarCartIcon widget, found in lib/widget/appbar_cart_icon.dart file.



This icon has a little bubble on it that keeps track of how many items are in the users’ cart. The widget is an IconButton widget, and its wired up to navigate to the cart page when it’s tapped and the onPressed callback is called:

 
 onPressed: () {     return
 Navigator.of(context).pushNamed("/cartPage");
 },
 

This is all there is to it.

NOTE

You may notice that Navigator.of(context).pushNamed(String routeName) function signature isn’t the same as the previously mentioned Navigator.pushNamed(BuildContext context, String routeName) signature. These are interchangeable.

 

MaterialDrawer widget and a full menu

If you’ve seen a Material Design app, you’re probably familiar with this type of app drawer:



Because this article is about routing, I think now is a good time to explore how to build that menu. First, let’s think about what we want it to do:

  1. The menu should display when a user taps a menu button.
  2. There should be a menu item for each page, which navigates to a route on tap.
  3. There should be an “About” menu item, which shows a modal with app information.
  4. There should be a menu header, which displays user information. When you tap on the user settings, it should route to the user settings page.
  5. The menu should highlight which route is currently active.
  6. The menu should close when a menu item is selected, or when a user taps the menu overlay to the right of the menu.
  7. When the menu is open or closed, it should animate nicely in and out.

This menu drawer is a combination of only five required widgets, all of which are built into Flutter.

  • Drawer
  • ListView
  • UserAccountsDrawerHeader
  • ListTile
  • AboutListTile

The Drawer is the widget that houses this menu. It takes a single widget in its child argument. You can pass it whatever you want (because everything is a widget). A Drawer will most likely be passed to a Scaffold in its drawer argument.

If you also have an AppBar in a scaffold with a drawer, then Flutter automatically sets the right-side icon on the app bar to a menu button, which opens the menu on tap. The menu animates out nicely, and closes when you swipe it left or tap the overlay to the right.

You can override the automatic menu button by setting

NOTE

You can override the automatic menu button by setting Scaffold.automaticallyImplyLeading to false.

Menu items: ListView and ListItems

Next, we have the ListView. A list view is a layout widget that arranges widgets in a scrollable container. It arranges its children vertically by default, but it’s quite customizable. For now, though, you can use it like you’d use a Column. You only need to pass it some children, which are all widgets.

A ListItem is a widget which has two special characteristics: They’re fixed height, which makes them ideal for menus. Also, the ListItem widget’s opinionated. Unlike other generalized widgets, which expect an argument called child, the Item has properties like “title”, “subtitle”, and “leading”. It also comes equipped with an onTap property.



The UserAccountsDrawerHeader is a Material widget which is used to display crucial information, and it listens to multiple events. Imagine a Google app like GMail, which lets you switch between user accounts with the tap of the button. This is built-in, and we don’t need it for this app. No lesson is learned here, but it’s a great example of how much Flutter gives you for free.

Finally, we have the AboutListTile. This widget can be passed into the ListView.children list, and configured in a few lines of code:

 
 AboutListTile(
     icon: Icon(Icons.info),
     applicationName: "Farmer's Market",
     aboutBoxChildren: <Widget>[
     Text("Thanks for reading Flutter in Action!"),
 ]),
 

With that code, you get a fully functional menu button that displays a modal on tap, complete with a button to close the modal. It looks like this:



With all that in mind, I think this brings our requirements list to this:

  1. There should be a menu item for each page, which navigates to a route on tap.
  2. The menu should highlight which route’s currently active.

This is slightly over-exaggerated. You need to write those five lines of code for the AboutListTile, but this is a heck of a lot easier than writing the logic to show a modal, and write the layout for the modal itself.

 

Implementing the Menu Drawer

With all this widget knowledge in mind, most of the work is done for you. The bulk of implementing this menu is in the routing. Which works well, because this is an article about routing. Most of this work is done in the lib/menu/app_menu_drawer.dart file.

For starters, menu items, when tapped, should route to a new page. In the build method, there’s a ListTile for each of these menu items. One ListTile looks like this:

Listing 4. Menu drawer item in a ListTile widget

 
 // e_commerce/lib/menu/app_menu_drawer.dart -- line ~63
 ListTile(
     leading: Icon(Icons.apps),
     title: Text("Catalog"),
     selected: _activeRoute == ECommerceRoutes.catalogPage, 
     onTap: () => _navigate(context, ECommerceRoutes.catalogPage), 
 ),
 

If selected is true, the ListTile configures its children’s colors to reflect that it’s the active route. I’ll talk about this more in depth in the next section.

ListTile.onTap is the perfect place to call a method which navigates to a new route.

_navigate looks like this:

 
 void _navigate(BuildContext context, String route) {     Navigator.popAndPushNamed(context, route); 
 }
 

Navigator.popAndPushNamed is another method to manage the route stack, like push and pushNamed. This method pops the current page off though, to ensure that there isn’t a giant stack of pages.

 

You can see another example of adding a page to the stack in the UserAccountsDrawerHeader widget in this same build method.

 

 
 UserAccountsDrawerHeader(
   currentAccountPicture: CircleAvatar(     backgroundImage:
         AssetImage("assets/images/apple-in-hand.jpg"),
   ),
   accountEmail: Text(s.data.contact),
   accountName: Text(s.data.name),
   onDetailsPressed: () {
     Navigator.pushReplacementNamed(
         context, ECommerceRoutes.userSettingsPage);
   },
 ),
 

Navigator.pushReplacementNamed ensures that the route stack won’t keep adding new pages. It’ll remove the route you’re navigating from when the new route is finished animating in.

 

Highlight active route with RouteAware

Another interesting aspect of the Flutter router is “Observers”. You won’t get far into Dart programming without using observers and streams and emitting events. I’m going to focus specifically on a NavigatorObserver. A navigator observer is an object that tells any widget who’s listening “hey, the navigator is performing some event, if you’re interested.” This is it’s only job – but it’s an important one.



A subclass of NavigatorObserver, and the one we’re interested in here is

RouteObserver. This observer specifically notifies all its listeners if the current active route of specific type changes. For instance, PageRoute.

This RouteObserver is how we’re going to keep track of which page is active, and use it highlight the correct menu item in the menu.

For many cases, you’ll only need one RouteObserver per Navigator. Which means there’s likely only one in your app. In this app, the route observer is built in the app file.

 
 final RouteObserver<Route> routeObserver =
 RouteObserver<Route>(); 
 

By giving it a type of <Route>, the observer notifies listeners of any route change.

Alternatively, <PageRoute> only notifies page routes.

I created the observer out in the open, not protected by the safety of any class, because I needed it to be visible throughout the whole app. It’s in the “global” scope.

After this, you need to tell your MaterialApp about it. In the same file:

Listing 5. Pass route observers into the MaterialApp widget

 
 // e_commerce/lib/app.dart -- line ~51 return MaterialApp(     debugShowCheckedModeBanner: false,
     theme: _theme,
     home: PageContainer(pageType: PageType.Catalog,),
     routes: { ... }
     navigatorObservers: [routeObserver], 
 );
 

Tell the MaterialApp about the routeObserver that it needs to notify when a routing event takes place.

This is all the setup, now you can listen to that observer on any State object. It has to be stateful because you’ll need to use some state lifecycle method. For our purposes, this is going to happen back in the AppMenu widget you’ve been working with in this section.

First, the state object needs to be extended with the mixin RouteAware.

 
 class AppMenuState extends State<AppMenu> with RouteAware { ... } 
 

with is the Dart keyword needed to use mixins.

This RouteAware mixin is an abstract class that provides an interface to interact with a route observer. Now, your state object has access to methods didPop, didPush and few others.

In order to update the menu with the correct active route, we need to be notified whenever there’s a new page on the stack. Two steps are needed to do that: First, listen to changes from the route observer. Second, listen to the observer to be notified whenever the route changes.

Listing 6. Listen to the route observer

 
 // e_commerce/lib/menu/app_menu_drawer.dart -- line ~19 class AppMenuState extends State<AppMenu> with RouteAware {   String _activeRoute; 
     @override
     void didChangeDependencies() { 
         super.didChangeDependencies();
         routeObserver.subscribe(this, ModalRoute.of(context)); 
     } // ...
 

The class variable I use to track the currently active route.

This is a widget lifecycle method, and the correct place to listen to new streams and observers.

You have access to the global routeObserver variable created. subscribe is a method that “listens” to the observer. This method expects a RouteAware object (which this state object is because it extends the RouteAware class, and the route in which you’re interested). In this case, the route you’re on now. A ModalRoute is a route that covers screen, even if it isn’t opaque on the whole screen. Any route that disables interaction with any route underneath it fits the definition. Examples are pages, popups, and drawers.

Now that this widget is aware of route changes, it needs to update its active route variable when any navigator activity happens. This is done in the didPush method that it inherited from RouteAware.

 
 // e_commerce/lib/menu/app_menu_drawer.dart -- line ~30 @override void didPush() {  _activeRoute = ModalRoute.of(context).settings.name; 
 }
 

This method is called whenever a route is pushed onto the stack. This happens as the menu itself is transitioning off screen. The next time you build the drawer, (by opening the menu), build is called again. Thus, it’s redundant to call setState.

That’s all for now. If you want to learn more about the book, check it out on liveBook here and see this slide deck.