Navigate Flutter Apps with Routes

One of the most important aspects of an app is the flow or journey that the user takes through the app. Apps are often described in terms of pages, or screens, and navigating between them. In this post I’m going to cover dividing your application into routes, and how to work with the Flutter navigation system.

Whilst a Flutter app is constructed out of widgets, there still needs to be a mechanism for the app to respond to user interaction. For example, tapping on an row in a list of items might display the details of that item. Subsequently tapping on the back button should return the user to the list of items. In order to support navigating between different pages, Flutter includes the Navigator class. According to the documentation the Navigator class is:

 A widget that manages a set of child widgets with a stack discipline. 

For someone who’s just got their head around how to layout widgets, this statement makes no sense. In this post we’re going to ignore this definition and cover the basics of how to navigate between pages. I’m also going to ignore the Flutter definition of a Route. For now we’ll assume that a route is roughly equivalent to a page or a screen in your app. As you can imagine, all but very basic apps have multiple pages that the user is able to navigate between. Flutter apps are no different except that you navigate Flutter apps with routes.

Flutter Navigation

Let’s get into navigating between routes. We’ll start with almost the most basic Flutter app you can create. Technically you can create a Flutter app that doesn’t start with the MaterialApp but then you’re left doing a lot of heavy lifting yourself. The following code sets up a basic app that is comprised of two classes that inherit from StatelessWidget. I could have combined these into a single widget by simply setting the value of the home attribute in the MaterialApp to be the new Container widget. However, I’ve created a separate widget, FirstNoRoutePage, to help make it easy to see how the pages of the app are defined.

void main() => runApp(MyNoRouteNavApp());

class MyNoRouteNavApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FirstNoRoutePage(),
    );
  }
}

class FirstNoRoutePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text('First Page'),
    );
  }
}

Ad-Hoc Routes with Navigator.push()

Now that we have the basic app structure, it’s time to navigate to our first route. We’re going to use the Navigator widget to push a newly created, or ad-hoc, route onto the stack. The following code:

  • Wraps the Text widget in a FlatButton
  • The onPressed calls the push method on the Navigator, passing in a newly constructed MaterialPageRoute.
  • The MaterialPageRoute builder is set to return a new instance of the SecondNoRoutePage widget
class FirstNoRoutePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: FlatButton(
        child: Text(
          'First Page',
          style: TextStyle(color: Colors.white),
        ),
        onPressed: () {
          Navigator.push(
              context,
              MaterialPageRoute(
                builder: (BuildContext context) => SecondNoRoutePage(),
              ));
        },
      ),
    );
  }
}

class SecondNoRoutePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(
        'Second Page',
        style: TextStyle(color: Colors.white),
      ),
    );
  }
}

We now have an application that has two pages that the user can navigate between. On the first page, clicking on the “First Page” text will push a new route that shows the second page (i.e. the SecondNoRoutePage widget). Pressing the device back button (Android only) will navigate the user back to the first page. It does this by popping the route off the stack maintained by the Navigator.

Go Back with Navigator.pop()

For the purpose of this post I’ve kept the pages simple, showing only elements necessary to show which page the user is on, or to allow the user to perform an action. I’ve chosen not to use a Scaffold, which means no AppBar. This also means no back button shown in the AppBar. On iOS this makes it impossible for the user to navigate to the previous page as there is no dedicated back button, unlike Android. If you use a Scaffold in your widget, you’ll see the AppBar. The AppBar adjusts to include a back button if there’s more than one route on the stack.

Flutter provides built in support for navigating back to the previous route via the AppBar back button or, in the case of Android, the device back button. In addition, the pop method on the Navigator can be used to pop the current route off the stack. This will return the user to the previous route.

class SecondNoRoutePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: FlatButton(
        child: Text(
          'Second Page',
          style: TextStyle(color: Colors.white),
        ),
        onPressed: () {
          Navigator.pop(context);
        },
      ),
    );
  }
}

What the Current Route?

Earlier in the post we created a new route when navigating to the second page of the app. However, what we glossed over is the fact that the first page of the app is also a route. To show this, let’s add some code that adds the name of the current route to both pages. This will highlight where in the Flutter navigation stack the user is currently at.

class FirstNoRoutePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var route = ModalRoute.of(context).settings.name;
    return Container(
      alignment: Alignment.center,
      child: FlatButton(
        child: Text(
          'First Page - $route',
          style: TextStyle(color: Colors.white),
        ),
        onPressed: () {
          Navigator.push(
              context,
              MaterialPageRoute(
                builder: (BuildContext context) => SecondNoRoutePage(),
              ));
        },
      ),
    );
  }
}

class SecondNoRoutePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var route = ModalRoute.of(context).settings.name;
    return Container(
      alignment: Alignment.center,
      child: FlatButton(
        child: Text(
          'Second Page - $route',
          style: TextStyle(color: Colors.white),
        ),
        onPressed: () {
          Navigator.pop(context);
        },
      ),
    );
  }
}

What gets displayed on the two pages are ‘First Page – /’ and ‘Second Page – null’. This indicates that the first route is assigned a name of ‘/’, whilst the second route does not have a name (i.e. null value). The first route is created from the home property being set on the MaterialApp. It is assigned a name equal to the defaultRouteName constant from the Navigator class (i.e. ‘/’) to indicate it is the entry point for the application.

As we created the route for the second page, it’s our responsibility to name the route. In the above scenario we’re creating the route at the point where it is required (i.e. when navigating to the second page), and as such we don’t need to give it a name. That is, unless we want to see that the current route is by inspecting it’s name. Let’s update the call to Navigator.push to include a name for the second route.

onPressed: () {
  Navigator.push(
      context,
      MaterialPageRoute(
        builder: (BuildContext context) => SecondNoRoutePage(),
        settings: RouteSettings(name: '/second')
      ));
},

There’s no requirement for the route name to include the ‘/’. However, it is a convention to do so, and there are some scenarios where the ‘/’ has some implied behaviour.

Named Routes in Flutter Navigation

In the previous section we added a name to the ad-hoc route that we created. An alternative to creating routes as they’re required, is to define the routes for the app up front. For example the following code shows three routes that have been defined for an app and can be used for Flutter navigation.

class Routes {
  static const String firstPage = '/';
  static const String secondPage = '/second';
  static const String thirdPage = '/third';
}

class MyNavApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        Routes.firstPage: (BuildContext context) => FirstPage(),
        Routes.secondPage: (BuildContext context) => SecondPage(),
        Routes.thirdPage: (BuildContext context) => ThirdPage(),
      },
    );
  }
}

The Routes class includes the names of each of the three routes. Inside the constructor of the MaterialApp widget the three routes have been declared. Each route is an association between the route name and the builder method that’s used to construct the widget.

Navigate.pushNamed()

When the user clicks the button, the Navigate.pushNamed method is used to navigate to the corresponding route:

onPressed: () {
  Navigator.pushNamed(context, Routes.secondPage);
},

Skipping Over Routes with Navigator.popUntil

Using named routes not only makes it easier to manage the pages in your app, it also means that you can create conditional logic that is dependent on the route name. For example, the Navigator.popUntil allows you to keep popping routes off the stack until the predicate is true. In the following code the predicate looks at the name property of the route to determine whether it is the first page of the app.

class ThirdPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: FlatButton(
        child: Text(
          'Third Page',
          style: TextStyle(color: Colors.white),
        ),
        onPressed: () {
          Navigator.popUntil(context, (r) => r.settings.name == Routes.firstPage);
        },
      ),
    );
  }
}

Setting the Initial Route

Initially the first page of the app was defined by setting the home property. This was used to define the initial route for the app. When the app was updated to use a set of predefined routes, the initial route was inferred by looking for the route where the name matches the defaultRouteName (i.e. ‘/’). If there wasn’t a route with that name isn’t found, an error will be raised and the app will fail to start.

It is also possible to set the initialRoute property on the MaterialApp. However, this doesn’t negate the need to set either the home property, or have a route declared with a name of ‘/’. What’s really interesting about the initialRoute property is that it can be used to launch the app on a route that has other routes in the navigation stack. For example in the following code the initialRoute is set to the thirdPage, which is defined as ‘/second/third’. When this app launches, it will launch showing ThirdPage but if the user taps the back button, they will go to SecondPage and then FirstPage if they tap again.

class Routes {
  static const String firstPage = '/';
  static const String secondPage = '/second';
  static const String thirdPage = '/second/third';
}

class MyNavApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: Routes.thirdPage,
      routes: {
        Routes.firstPage: (BuildContext context) => FirstPage(),
        Routes.secondPage: (BuildContext context) => SecondPage(),
        Routes.thirdPage: (BuildContext context) => ThirdPage(),
      },
    );
  }
}

On app startup the initalRoute is split on ‘/’ and then any routes that match the segments are navigated to. In this case the app navigates to FirstPage, then SecondPage and finally ThirdPage.

Intercepting Back Button

The last topic for this post looks at how to intercept the back button. This can be done by including the WillPopScope widget and returning an appropriate value in the onWillPop callback.

class ThirdPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () {
        return new Future.value(true);
      },
      child: Container(
        alignment: Alignment.center,
        child: Text('Third Page'),
      ),
    );
  }
}

Note that the onWillPop expects a Future to be returned, allowing you to return an asynchronous result. In this case the code is simply returning the value of true to allow the current route to be popped.

Summary of Flutter Navigation

This post on Flutter navigation has covered a number of the navigation related methods on the Navigator class. As part of defining how your app looks, you should define all the routes and how the user will navigate between them. If you do this early in the development process you’ll be able to validate the flow of your app as you continue development.

Productivity Musings on App Navigation, Information Density and Work Environment

This morning I set out to explore the most recent ramblings on app navigation. I expected to come across a bunch of designers talking about their sliding, popping, whirling interface. How they are going to radicalize the way we interact with mobile applications. The first thing I was struck with was that I needed to get out of my own mobile-centric thought process. I immediately extended my investigation to include desktop, tablet and of course web. After coming across various sites about different navigation constructs, my thought process wandered off. I soon found myself reflecting on some of the design decisions that we live and work with daily. This lead me to some productivity musings that I think generate more questions than answers.

Mobile is Not Desktop

The launch point for my productivity musings was that a couple of the sites reminded me of the panorama and pivot controls from Windows Phone. I think some credit should go to Microsoft’s Metro Modern Fluent (whatever it’s now called) design system. The Panorama and Pivot controls, when done well provided an immersive experience that let users rapidly dig into information.

Fast forward to Windows 8 and Microsoft decides to expand the Windows Phone design system to big-Windows….. #FAIL. We tried. We really did try. And in some cases we produced some amazing applications. However, fundamentally the design system wasn’t designed to scale. The Panorama didn’t have the right information density for a desktop application. One of the reasons that the Mail and Calendar apps for Windows were never able to take on Outlook, is that the information density is just too low.

Stupid Apps

I feel we would be remiss for not chastising Apple for a generation of “stupid apps”. When the iPhone came out, it set about revolutionizing what the world meant when it said smartphone. It was like all the devices prior to it weren’t smart. This simply isn’t true – there were plenty of devices in market that from a functionality perspective were smart. What Apple did was to bring smartphones to the masses. They did that by dumbing it down. Apps did one thing. You could run one App at a time. You can only get Apps from the App Store. There was only one devices that had Apps, the iPhone.

This lead the way for a new breed of developers, “App Developers”. These were developers who were able to string a couple of screens together and call it an app. These developers were in a league of their own. Web and desktop developers looked over and saw “App Developers” making money from apps that did one thing. Ina lot of cases the apps didn’t even do that one thing well, in other words, stupid apps.

Over time the mobile ecosystem has evolved. App Developers have matured and today most app developers build apps for both iOS and Android. Apps themselves have evolved and increased in complexity and sophistication. AI and Machine Learning is being integrated to allow mobile applications to solve a range of tasks. However, despite all this innovation, the majority of mobile apps still do only a small number of functions.

Information Density

As my productivity musings continue, if we look at apps designed for desktop, we can see that they have more functions and much higher information density. Apps such as Word, Excel, VS Code and Photoshop are heavily optimised for mouse and keyboard. As such there is a much high information density, allowing more data to be presented and manipulated on any given screen.

Of course, there’s a trade off between ease of use and information density. Actually, let me correct that somewhat. It’s not necessarily ease of use that decreases with increase information. Rather, it’s the ability for new users to learn an application. Take Excel for example. There are plenty of spreadsheet alternatives out there. I’m continually amazed by the number of people that use Google Sheets. This is because they’ve only ever learnt the first 10% of spreadsheet capabilities. Those that you pick up when you first start using a spreadsheet. Most users barely get past using a spreadsheet as a glorified list making tool. If you look at how spreadsheets are used in finance or engineering, it’s staggering how much data can be calculated, displayed and interpreted on a single screen.

What struck me about the links that I read regarding navigation, is that very few of them deal with high information density. All the designs seem to be focused on building slick interfaces that work well with a single hand, or respond well when the browser is resized. There’s almost no mention of how to deal with complex data sets, or multiple windows, or the learning process for complex applications.

Multiple Windows

At this point, my productivity musings turned towards a pet annoyance, which is the lack of support for multiple windows. Since around the time of Windows 8, it seems that most developers have forgotten that both PC and Mac support applications with multiple windows. Both Apple and Microsoft realise the significance of the window metaphor. Whilst it was Microsoft that adopted the name Windows for the OS, both companies continue to support and evolve the window metaphor.

It should be noted that in Windows 8, Microsoft made a daft move by trying to push apps full screen. How did it make sense to have an operating system called Windows that doesn’t support applications running in a windows? Unfortunately I think the lasting impact of this, coupled with the birth of app developers who knew how to build “stupid apps,” has meant that we’ve had to put up with a host of single-windowed apps.

Take for example this comment from Derik about wanting to pop out an editor window to drag to another screen:

Desktop apps, for both PC and Mac, need to start by thinking in multiple windows. Don’t just make it an after thought.

Window Management

We’re approaching the end of my productivity musings. At this point I went on a slight tangent along the idea of multiple windows and thinking about the way that they’re managed. Both Windows and MacOS have mechanisms for closing, opening, restoring, minimising, splitting windows. However, the one thing I routinely notice among Mac users is that they struggle with basic window management. Perhaps they’ve never bother to spend the time to work out how to manage windows; perhaps it’s harder to do on a Mac. Personally I stick with Windows as that’s where I’m comfortable. If you can’t manage your application windows effectively, that’s on you to learn how to do it.

Multi-Monitor

Moving on from multiple windows we get to multiple monitors. This one is a criticism of companies that choose to invest in finding the right staff but then fail to equip them to do their job. If your staff do work that’s heavily computational, make sure they have a desktop PC or Mac to work on. Regardless of whether you equip them with a desktop or a laptop, when your staff are working at a desk, they need to have at least two external monitors to work on and ideally external mouse and keyboard. You’ll spend a little more up front setting up their work station but you’ll get that back in productivity within the first month, if not the first week!

That’s Not a Mouse!

If you decide that you’re going to supply MacBook or iMac or some other Apple product to your staff, please make sure you do the right thing and buy them a real mouse and keyboard set.

A mouse has more than just a single function. Go get a mouse that will really get the job done.

A keyboard isn’t a work of art. Go get a keyboard that will really make you productive. It doesn’t have to be split like in the following image but it is worth considering split keyboards as they are supposed to be more ergonomic.

Productivity Musings in Summary

This post is a bit of a ramble about the hits and misses of app development. However, it’s worth consider where we are in the software development industry. Are we so focused on the latest technology; the latest hot trend; the latest design metaphor, that we’ve lost the ability to build productivity software?


App Navigation Links

The following are a selection of links that I browsed when investigating some of the latest trends when it comes to navigation within applications. Whilst they’re not strictly a productivity musing, they did form the genesis for this post and I’d recommend taking a read as they may influence how you design the experience of your next application.

ViewModel to ViewModel Navigation in a Xamarin.Forms Application with Prism and MvvmCross

ViewModel to ViewModel Navigation in a Xamarin.Forms Application with Prism and MvvmCross

I’m a big fan of the separation that the Mvvm pattern gives developers in that the user interface is encapsulated in the view (Page, UserControl etc) and that the business logic resides in the ViewModel/Model. When structuring the solution for an application I will go so far as to separate out my ViewModels into a separate project from the views, even with Xamarin.Forms where the views can be defined in a .NET Standard library.

One of the abstractions that this lends itself to is what is referred to as ViewModel to ViewModel navigation – rather than the ViewModel explicitly navigation to a page, or bubbling an event up to the corresponding view to get the view to navigate to the next page, ViewModel to ViewModel navigation allows the ViewModel to call a method such as Navigation(newViewModel) where the newViewModel parameter is either the type of the ViewModel to navigate to, or in some frameworks it may be an actual instance of the new ViewModel.

MvvmCross

Let’s see this in action with MvvmCross first – I’m going to start here because ViewModel to ViewModel navigation is the default navigation pattern in MvvmCross. I’ll start with a new project, created using the MvxScaffolding I covered in my previous post, using MvvmCross in a Xamarin.Forms application. The single view template already comes with a page, HomePage, with corresponding ViewModel, HomeViewModel. To demonstrate navigation I’m going to add a second page and a second ViewModel. Firstly, I’ll add a new class, SecondViewModel, which will inherit from BaseViewModel (a class generated by MvxScaffolding which inherits from MvxViewModel that’s part of MvvmCross).

public class SecondViewModel : BaseViewModel
{
}

Next, I’ll add a new ContentPage called SecondPage (note the convention here that there is a pairing between the page and the ViewModel ie [PageName]Page maps to [PageName]ViewModel)

image

MvvmCross supports automatic registration of pages and ViewModels but it does require that the page inherits from the Mvx base class, MvxContentPage. I just need to adjust the root XAML element from

<ContentPage …

to

<views:MvxContentPage x_TypeArguments=”viewModels:SecondViewModel” …

The inclusion of the TypeArguments means that the generic overload of MvxContentPage is used, providing a helpful ViewModel property by which to access the strongly typed ViewModel that is databound to the page.

Now that we have the second page, we just need to be able to navigate from the HomePage. I’ll add a Button to the HomePage so that the user can drive the navigation:

<Button Text=”Next” Clicked=”NextClicked” />

With method NextClicked as the event handler (here I’m just using a regular event handler but in most cases this would be data bound to a command within the HomeViewModel):

private async void NextClicked(object sender, EventArgs e)
{
     await ViewModel.NextStep();
}

And of course we need to add the NextStep method to the HomeViewModel that will do the navigation. The HomeViewModel also needs access to the IMvxNavigationService in order to invoke the Navigate method – this is done by adding the dependency to the HomeViewModel constructor.

public class HomeViewModel : BaseViewModel
{
     private readonly IMvxNavigationService navigationService;


    public HomeViewModel(IMvxNavigationService navService)
     {
         navigationService = navService;
     }
     public async Task NextStep()
     {
         await navigationService.Navigate<SecondViewModel>();
    }
}

As you can see from this example the HomeViewModel only needs to know about the SecondViewModel, rather than the explicit SecondPage view. This makes it much easier to test the ViewModel as you can provide a mock IMvxNavigationService and verify that the Navigate method is invoked.

Prism

Now let’s switch over to Prism and I’ve used the Prism Template Pack to create a new project. To add a second page I’ll add a SecondPageViewModel, which in the case of Prism inherits from ViewModelBase and requires the appropriate constructor that provides access to the INavigationService. Note that the naming convention with Prism is slightly different from MvvmCross where the ViewModel name is [PageName]PageViewModel (ie both the page and the viewmodel have the Page suffix after the PageName eg SecondPage and SecondPageViewModel)

public class SecondPageViewModel : ViewModelBase
{
     public SecondPageViewModel(INavigationService navigationService) : base(navigationService)
     {
     }
}

I’ll add a new ContentPage called SecondPage but unlike MvvmCross I don’t need to alter the inheritance of this page. Instead what I do need to do is register the page so that it can be navigated to. This is done in the App.xaml.cs where there is already a RegisterTypes method – note the additional line to register SecondPage.

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
     containerRegistry.RegisterForNavigation<NavigationPage>();
     containerRegistry.RegisterForNavigation<MainPage, MainPageViewModel>();
     containerRegistry.RegisterForNavigation<SecondPage, SecondPageViewModel>();
}

Similar to the MvvmCross example, I’ll add a button to the MainPage (the first page of the Prism application created by the template) with code behind to call the NextStep method on the MainViewModel

private async void NextClicked(object sender, EventArgs e)
{
     await (BindingContext as MainPageViewModel).NextStep();
}

Note that because the MainPage just inherits from the Xamarin.Forms ContentPage there’s no property to expose the data bound viewmodel. Hence the casting of the BindingContext, which you’d of course do null checking and error handling around in a real world application.

public async Task NextStep()
{
     await NavigationService.NavigateAsync(“SecondPage”);
}

The NextStep method invokes the NavigateAsync method using a string literal for the SecondPage – I’m really not a big fan of this since it a) uses a string literal and b) requires the the ViewModel knows about the view that’s being navigated to. So let’s adjust this slightly by changing the way that pages and ViewModels are registered. The RegisterForNavigation method accepts a parameter that allows you to override the navigation path, meaning we can set it to be the name of the ViewModel instead of the name of the page.

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
     containerRegistry.RegisterForNavigation<NavigationPage>();
     containerRegistry.RegisterForNavigation<MainPage, MainPageViewModel>(nameof(MainPageViewModel));
     containerRegistry.RegisterForNavigation<SecondPage, SecondPageViewModel>(nameof(SecondPageViewModel));
}

The navigation methods would then look like:

public async Task NextStep()
{
     await NavigationService.NavigateAsync(nameof(SecondPageViewModel));
}

But I think we an improve this further still by defining a couple of extension methods

public static class PrismHelpers
{
     public static void RegisterForViewModelNavigation<TView, TViewModel>(this IContainerRegistry containerRegistry)
         where TView : Page
         where TViewModel : class
     {
         containerRegistry.RegisterForNavigation<TView, TViewModel>(typeof(TViewModel).Name);
     }


    public static async Task<INavigationResult> NavigateAsync<TViewModel>(this INavigationService navigationService)
         where TViewModel : class
     {
         return await navigationService.NavigateAsync(typeof(TViewModel).Name);
     }
}

Using these extension methods we can update the registration code:

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
     containerRegistry.RegisterForNavigation<NavigationPage>();
     containerRegistry.RegisterForViewModelNavigation<MainPage, MainPageViewModel>();
     containerRegistry.RegisterForViewModelNavigation<SecondPage, SecondPageViewModel>();
}

And then the navigation code:

public async Task NextStep()
{
     await NavigationService.NavigateAsync<SecondPageViewModel>();
}

The upshot of these changes is that there’s almost no difference between the MvvmCross method of navigation and what can be done with a little tweaking with Prism.