Spotless Routing and Navigation in Flutter
As soon as you’ve finished studying the basics of Flutter and start working on your first projects, you’ll find yourself asking:
“What’s the best way I can handle routing and navigation?”
“Should I use named routes? Should I use a package?”
No, I’m not here to say “it depends on your needs.”
I work at a software service company, and as such, we develop many apps a year. That puts a demand on finding solutions that fit the largest number of different projects.
If you’ve done native mobile development, you know that routing always becomes a serious matter. Whenever I’m around iOSers, it’s five minutes before we’re discussing routers, coordinators, flows, and their pros and cons. With Androiders, it’s exactly the same — just replace the terms with Jetpack Navigation, Cicerone or FragNav. It seems like no one is ever completely satisfied with it.
If Flutter is your first foray into the mobile world, remember these words: This won’t be the last time you talk about routing strategies.
And before you get hopeless about it, just give me a chance to show you how I’ve been doing it.
I’m completely satisfied and I think you will be too.
Have you ever taken time to think about how navigation is typically made on the web?
I’m not talking about how it’s developed or anything like that — I’m interested in your experience as a website user or RESTful API consumer.
URLs are typically designed around resources, with a resource being a collection or a single item. For example:
Want to access a list of movies? Go to ${yourFavoriteMovieWebsite}/movies
.
Want to see more details about a specific movie you found on the list? Go to ${yourFavoriteMovieWebsite}/movies/:picked_movie_id
.
Did you notice the parametrized ID? That’s a path param or URL param.
What about seeing only the movies released in 2020? No problems, just pass in a query param called releaseYear to the first endpoint:
${yourFavoriteMovieWebsite}/movies?releaseYear=2020
By the way, that part of the URL containing the parameter, starting with the question mark, is what we call a query string.
Further reading: REST Resource Naming
What if We Could Do the Same With Our Apps?
“Why would I want such a thing?”
Apart from it being amazingly intuitive and organized, we’re just one step behind being ready for deep links.
“Deep links?”
Have you ever opened a link on your phone that, instead of opening a web page, opens a specific page of an app?
Another good example is when you receive a push notification from a messenger app and tapping on it, it takes you to that specific conversation screen.
That’s what deep linking is all about. That’s also all we’re saying about it for today — it will have an article of its own soon.
My point is this: If we’re going to think of URL paths to our pages sooner or later, why not do it from the beginning? Even if you don’t have any intention of supporting deep links in your project, this is still the most well-established way of organizing routes.
To showcase today’s article, I decided we needed to work on a real app. To make it possible, I spent some time looking for free HTTP APIs, that’s when I found the great Breaking Bad API.
It provides us with a collection of information on the Breaking Bad and Better Call Saul TV Shows. Our app, though, is restricted to Breaking Bad characters and its remarkable quotes.
That settled, I found myself with the most challenging task of it all. Deciding what the app’s name would be!
The Breaking Bapp Was Born
Don’t worry about my marketer skills — I don’t intend to follow that career! Also, I promise you I’ll do my best to keep the “breaking” part only in the name.
Thinking in Resources
We already stated that we should design our paths around the resources we’re providing. Can you imagine what the URLs would look like if we were talking about a website? I did that exercise:
-
List of characters:
characters
-
Details of a single character:
characters/:character_id
-
List of quotes:
quotes
-
Details of a quote author:
quotes/authors?name=:quote_author_name
The last one is the trickiest. Maybe you’re wondering why name
is a query param instead of a path param, or why we aren’t using the quote’s author ID instead of their name.
To answer that, let me first show you a quote’s JSON object retrieved from the API:
Notice that we don’t have the ID of the author, just its name. Since name
isn’t a reliable unique identifier and path params are recommended for unique identifiers, the best thing we can do is thinking as if we’re querying the author’s resource.
And, we have the plus that by designing it like this, I can teach you how to work with query strings as we go.
It’s important to mention, for those of you who are not Breaking Bad fans, that a quote author is nothing but a character of the show. So, to obtain this info, we just need to use the character details endpoint — fortunately, the API allows us to find a character by its name instead of its ID.
How Does Navigation Work in Flutter?
Navigating is nothing but the simple act of transitioning to a new full-screen widget, which we call a page or a screen. To do so, we need to wrap this new widget inside a Route
object, that describes how the transition should happen, and push it to a Navigator
. There are two ways to do this, by default.
First method
Give the Navigator
a Route
object.
Bringing it to the Breaking Bapp, this is how it would look like for the onTap
callback of the character’s list.
Extremely easy to learn;
Strongly typed;
Leads to lots of code duplication if you need to navigate to the same screen in many parts of your app;
Second method, a.k.a. named routes
Give the Navigator
a route name and a generic argument (optional), which will be delivered to a function that maps these two to a Route
.
You need to write that function by yourself and use it as the onGenerateRoute
of the MaterialApp
.
Not as easy to learn as the first method.
Although it looks more verbose, it is actually the recommended approach, because we keep our page’s instantiation centralized, avoiding code duplication.
If we take a closer look, the named route approach is relatively close to the web-thing we’re trying to accomplish. The only difference is that on the web, we wouldn’t need this argument object (the third param of the Navigator.pushNamed
), as we would be able to pass every parameter we need inside the route name, either as path param or as a query param.
We want to do this: Navigator.of(context).pushNamed('characters/${character.id}');
Instead of this: Navigator.of(context).pushNamed('character-details', arguments: character.id);
However, if you try to do this right away, it will give you a headache. Inside your onGenerateRoute
function, you would need a way to wildcard the character’s ID, so that characters/11
and characters/22
would take to the same route, just with different arguments. Another problem is having to strip and parse whatever comes after the question mark as query parameters.
Fluro Package to the Rescue
According to them (and I’m inclined to agree), Fluro is:
The brightest, hippest, coolest router for Flutter.
Among several features, I would highlight:
-
Wildcard parameter matching
-
Querystring parameter parsing
We found our guy!
For our starting point, I built a version of the Breaking Bapp using the first routing method (without named routes), from which we’ll evolve to an elegant solution using Fluro.
Don’t worry about how I made the HTTP calls or what is the state management approach. I’ve managed to keep things as simple as possible so that it won’t take the focus of the navigation and routing.
To cover a scenario with multiple Navigator
s, we have a bottom navigation menu structure.
I suggest you check out the starting-point
branch of the Breaking Bapp’s GitHub repository and get relatively comfortable with it for a better learning experience.
Step-by-step on Migrating to Fluro
1. Install Fluro
The article is based on version 1.6.3.
Please, don’t use a newer version until you’re finished with all the steps. I can’t state how important that is.
https://pub.dev/packages/fluro#-installing-tab-
2. Set up your routes
The main Fluro’s class is called Router
. We’ll use it for everything we need to do, except for pushing pages, as we’ll continue to use the Navigator
for that. You don’t need to instantiate it by yourself, as Fluro already made an instance available to us as a static variable, simply accessed by Router.appRouter
.
First things first, we’ll start using a Router
’s function called define
, which serves the purpose of, guess what, defining a route. It takes in three arguments:
-
A String representing the route’s path along with its expected path parameters. The query ones don’t need to be specified ahead.
-
A Handler object which holds the page’s widget builder.
-
An optional
TransitionType
, just in case you want to change the default route transition.
I did all my routing set up inside the main function (main.dart
), and I suggest you do the same.
Read the Gist below carefully. I made an effort to comment everything you need to know.
3. Link your router to your navigators
Now we have a special little guy called Router.appRouter
that knows how to handle all of our web-like named routes. But we haven’t plugged it anywhere. Because of that, when we Navigator.pushNamed
something, it still won’t use all the setup we just did in the previous step.
Do you remember the MaterialApp.onGenerateRoute
constructor parameter? We’ve talked about it briefly when discussing named routes. We need to provide it with a function that receives the pushed route settings and uses it to build a concrete Route
. Guess who has one of these? Yes, the Router.appRouter
.
Now, every time we navigate using our root Navigator
, everything works just fine.
“What do you mean by root Navigator?”
By default, inside MaterialApp
, we’re given a Navigator
to push and pop our pages widgets, and this is the one we’ve just linked with our Router
. But if your navigation structure is a little more complicated than that, for example if you use a bottom navigation component, you probably have more than one Navigator
. We need to link those to Fluro as well. Fortunately that’s the Breaking Bapp’s scenario!
“How can I make Fluro work with multiple Navigator
s?”
Short answer: The Navigator
’s constructor also has an onGenerateRoute
. Just give it the same [Router.appRouter.matchRoute(context, settings.name, routeSettings: settings).route
] everywhere you instantiate a new Navigator
in your code, use the initialRoute
parameter for changing its initial route, and skip to our fourth step.
Long answer: For this, I’m going to show you how I did it in the Breaking Bapp’s bottom navigation.
If you haven’t followed my latest article, it won’t be easy for you to understand what’s going on. If that’s the case, I strongly suggest you be satisfied with the short answer and skip to the fourth step. I promise you there will be no losses.
3.1. Linking your router to your additional navigators
Our code has a class called BottomNavigationTab
that holds all the dependencies for building our app’s bottom navigation flows. That includes the item to be displayed in the menu, the GlobalKey
it should use for that specific tab’s Navigator
, and the initial page builder for each. The initialPageBuilder
was useful once when we were instantiating the routes ourselves (by using the first navigation method). But now that we’re using named routes, we don’t like page builders anymore, just route names, as Fluro is the one building things now.
Here’s the diff:
As we just changed the BottomNavigationTab
’s definition, we need to fix the place we’re instantiating it:
There are two more places left, both inside the material_bottom_navigation_scaffold.dart
, as that’s where we’re subclassing and instantiating the above BottomNavigationTab
.
Finally, it’s time to ensure that our additional navigators delegates its route generation to Fluro, as well as using the newest initialRouteName
of the BottomNavigationTab
. I’m using different bottom navigation menus for Android and iOS, so I needed to make that change in two files:
material_bottom_navigation_scaffold.dart
cupertino_bottom_navigation_scaffold.dart
4. Navigate
Everything we’ve done by now was about being prepared to handle any incoming web-like named routes, but we’re still not using them. We need to stop using Navigator.push
and start using Navigator.pushNamed
. There are two places for changing that:
- When selecting a character on the list.
character_list_page.dart
- When selecting a quote’s author on the list.
quote_list_page.dart
We’re all done!
Side note
I once read someone arguing that with Fluro, we’re not able to pass complex objects directly from one page to another. Although we can find obscure ways to do that, I don’t advise on doing so.
I firmly believe that we shouldn’t be passing complex objects around. An alternative I prefer is giving the object an id
and storing it in your data layer. That way, we can pass only its id
between pages.
Bonus
You may have noticed that the route names are duplicated across the code. This isn’t a recommended practice at all. It served the purpose of improving the article’s readability, but I don’t want you to do that anywhere else. For inspiration on how to extract it, check out the master
branch.