Explaining Flutter Nav 2.0 and Beamer
Introduction
I am fairly new to Flutter and come from a web development background. In late 2020 Flutter introduced Nav 2.0 which is primarily designed to allow Flutter apps to behave more like websites do when the app is accessed through a web browser.
However this is not an easy task and the interface for Nav 2.0 appears overly complicated, is not that well documented and is tangled up with old ways of navigating.
I wrote my own code to use Nav 2.0 but then found the Beamer package which makes everything easier and clearer.
This article tries to give an understanding of what Nav 2.0 is for, why this is advantageous and how Beamer solves the problem of complexity found in the base API. It is the article I wish I could have read when I first started investigating Nav 2.0.
Background to Navigation and the Problem with URLs
Navigation is something that has evolved with Flutter, so there are lots of different ways of doing things.
Pages are wrapped in routes which are pushed onto and popped off of a navigation stack. This suits a mobile app navigation paradigm, especially Android which has a back button. But on all platforms there is a sense of drilling deeper into screens and then there is usually a back arrow (eg in the AppBar) that allows the user to return to the previous page.
Making apps available through web browsers complicates this as the navigational paradigm is different with forward and back buttons plus the browsing history, bookmarks and the ability to navigate directly to pages by typing a url. Web pages often offer some in screen navigation such as menus or other links, which is fine when you have lots of screen space but a UI that is to work on both mobile and web cannot afford all that screen real estate.
At first urls appear to be simply identifiers like named routes. However, in the past, an app page might only have been reached by traversing a certain route through the app screens. Allowing a user to jump straight into a details screen without first going through a list of items might never have been possible before. So the app needs to know what the context of a page is straight away without actually having to traverse the intermediate pages. ie if landing directly on a details page the back button needs to know that is has to go “back” to a list it has not come from.
This is my understanding of what is what is meant by a declarative navigation rather than imperative. Before Nav 2.0 to get to a “state” where you are looking at a details page you have to follow the procedure of going to the list page and then tapping on the item to show the details page. Now you can get there directly but you declare the “state” of the detail screen as being above a list screen even though you did not go through it.
Enter Nav 2.0
There is a dilemma for Nav 2.0 examples which is they are too simple to represent a real world case, and yet the code for even these looks very complicated and dosen’t seem to give a lot that wasn’t possible with the old routes.
One example I found which helps to show the core ideas is this video by Kilo Loco. What makes this good is he is not dealing with the app in a browser and the code is not dealing with urls, it is simply looking at how pages work in the new Navigator.
This app has a simple list of “users” and tapping an item goes to a details screen for that user. The back button returns to the list. Very simple, and we could easily do this with old nav. However, by sticking to just the new “pages” list we can see the key idea of Nav 2.0.
Here is the central code of the app, the actual list view and detail view screens are not important to understanding what is going on here at the core.
Normally one uses the Navigator widget that is built into the MaterialApp and CupertinoApp so we don’t often see it declared explicitly like in the above code. This is still the same Navigator and it can still do all of the things the old Navigator could do (liked routes: and onGenerateRoute: and so on), but there are a few extra parameters that implement the new 2.0 functions.
pages:
The Navigator has a “pages” parameter. Each entry of this list is a MaterialPage(). Each MaterialPage holds a child which builds a page, and usually has a key which allows the Navigator to identify the page uniquely for efficient rebuilds. There must be one or more items in the pages list.
Once the build method has finished, the navigator takes the list of pages and turns them into a stack of routes. This is the key difference between declarative and imperative. We have declared a state as a list of pages and the Navigator makes it so. We have not built the stack ourselves through the traversal of the app (although traversal of the app is one way of getting to the state that we have just declared).
onPopPage:
With Nav 2.0 we are always working with a state that describes the navigation. In this example, Kilo is holding that state in the app which is a StatfefulWidget. The state is simply the ID of the selected user. If the ID is null we show the list page, and if the ID contains a value we show the details screen for the user with that ID.
This ID gets set by the callback method handed to the list view to use on an item onTap event.
However the state needs to change and the ID set to null when the page is popped.
This is what the onPopPage parameter is for. It is called with the details of the page being popped. You could return false to stop the pop and halt the navigation but mainly it is used to change the state.
In this example, the code gets the page details being popped, it checks if this is the user details page and, if it is, sets the ID in the state back to null. The method also has to return true to allow the page to be popped by the system. This is best done using the “route.didPop” function on the route.
And that is the basic Nav 2.0 concept: take a state, return a list of pages, pages become routes, top route displayed.
Making It More Complicated
The above example is very simplistic but illustrates the core concepts. It does not even make urls for the pages in the app. In the real world we will have lots more pages, more complex states, deeper traversal routes through an app, access from different platforms and so on.
As with many other widgets the simple case can just add widgets as children, but for the complex cases you need a delegate function to find or create more diverse or complex child items. And so it is with Navigator 2.0.
Rather than adding a Navigator to the home property of a MaterialApp, we have to use a named constructor MaterialApp.router() as our main app. This constructor takes two extra parameters:
1 — routerDelegate
The routerDelegate has a build method and returns a Navigator widget just like we saw in the simple example above. It also has to have a setNewRoutePath() method which takes a description of a location in the app (a state) and the delegate build method then turns this state into a stack of pages.
2 — routeInformationParser
The routeInformationParser is a class with methods to turn a web URL into an internal state that can be passed to the delegate via setNewRoutePath(). It also has a method to “restore” an internal state back into a web URL.
The sample code for implementing these can only be described as tortured. There is lots of boiler plate code, lots of ChangeNotifier handling and notifying of listeners. All examples use a page manager class outside of the routerDelegate which requires all of these notifications.
In other words if you are going to implement a real world app with real world complexity, the implementation is going to be a nightmare.
Making It Simple Again — Beamer
The idea with Nav 2.0 is being able to navigate by declaration which Beamer sees as being like Star Trek teleporting. “Beam me up Scotty!” That is, take the user directly where they want to go without intermediate screens or steps.
To go to a location in the app simply use a command like this:
Beamer.of(context).beamToNamed(“/books/2”);
The documentation is very good and there are lots of examples showing different ways of using the package. The rest of this article will explain some of the higher level concepts that will help you understand what you might do with Beamer and how to approach it in code.
BeamLocation
The most important class in Beamer is the BeamLocation. This is an abstract class so you will always define your own types of location from this class. Each of these BeamLocations holds the template urls, the pages list generator, and a state which governs which pages are placed in the routes stack and displayed.
If you have several different sets of pages, eg one set for books, another set for articles and a third for films you would probably want to define three BeamLocations, each collecting together the pages for those different items.
The above is a very simple “home > books list > books detail” example. However you might have a more complex structure where there is a list of genres and authors. A user might go from a book detail to a list of genres and then to a list of books in the same genre. Or perhaps from a book to an author and then to a list of books by author. Handling that sort of traversal with old routes would be trickier but Beamer makes it easy.
Here is the BeamLocation for this more complex book example.
The above structure would allow the user to navigate paths like this:
Home Screen > list of all Books > specific book > list of genres for current book
Up to this point the user could simply click the back button in the AppBar to reverse their steps on-by-one. However the screen showing the list of genres for the book allows the user to click on a genre name which then displays a list of books in that genre.
As this is another list of books the logic in the BookLocation will remove the intermediate pages in the stack, so that if the user taps the back button on the AppBar they will return to the home screen. This makes sense because the user may drill into anther book and look at its genres and a list of books from that. The traversal path would get very deep if it did not “reset” whenever another list of books was shown.
A similar traversal can be done drilling down by author as well.
pathBlueprints
The pathBlueprints set out the structure of the paths (urls) that would describe these pages. When Beamer gets a new path from the browser location bar or from a call to beamToNamed() it parses the incoming path and matches it to the pathBlueprints in the location. Any path segments beginning with colons represent variable sections or pathParameters. For instance a pathBlueprint of ‘/books/:bookId’ would match a uri of ‘/books/2’ and the state would be:
state.uri.pathSegments = [‘books’, ‘2’]state.pathBlueprintSegments = [‘books’, ‘:booksId’]state.pathParameters = {‘bookId’: ‘2’}
If there is more than one pathBlueprint, Beamer tries each one until one matches. If none of them matched the state will still include the state.pathBlueprintSegments which will be the same as state.uri.pathSegments but state.pathParameters will be null.
PathBlueprints can use an asterisk “*” wildcard to represent a whole segment of any value (eg “/books/*” but not partial names like “/b*”). You can also use “/*” to match any path.
It is also worth mentioning that the state will also hold any query keys and values from a uri in state.queryParameters as a map of names and values. A “query” is the part of a url after the question mark.
pagesBuilder
The other key part of the BeamLocation is the pagesBuilder function which returns a list of BeamPages. This function is called whenever the current state changes and this location is active (more on that later).
The list of BeamPages returned are converted by Beamer (and Nav 2.0) into a stack of routes with the final page being the one displayed. You can build the list any way you like but it is usually easiest to add conditionals into the list declaration as seen in the above examples.
You use the values in the state to determine which pages should be included in the list.
BeamPages must have a child which is the widget that draws a screen or page. For efficiency it is a really good idea to add a key to each page so that rebuilds can be optimised and pages with matching keys do not need to be rebuilt each time.
You can include a “type” value which determines the transition type when the page is displayed and removed. There are constants for: cupertino, fadeTransition, material and noTransition.
BeamerRouterDelegate
As discussed in the “Making it complicated” section above, a routerDeligate is used by Nav 2.0 to handle building the pages list and installing a Navigator into the widget tree to control the navigation. This along with a route parser have to be added to the MaterialApp.router() or CupertinoApp.router() at the top of your app.
Beamer has methods to use here like this:
The Beamer documentation covers a couple of simple locationBuilder options which may suffice for your app:
SimpleLocationBuilder — which can wrap up a list of routes like in the old navigation and you don’t even have to define any BeamLocations.
beamLocations — which is simply a list of BeamLocations you have defined in the app. Beamer will work out when to use each Location based on the pathBlueprints.
But ideally you will have defined your BeamLocations and can then write your own code for the locationBuilder to choose the appropriate location based on the current BeamState which is passed into the method:
It is worth noting that a BeamerRouterDelegate has a parameter “initialPath” which is a string to set the initial path used in the state.
How This All Works Together
What takes a little time to get used to is how all these components work together and to understand what drives the process. In this discussion I will not consider the SimpleLocationBuilder or the BeamerLocationBuilder as locationBuilders and will assume you are writing your own code to handle this.
Fundamentally any navigation in the app happens because of a change in the state (BeamState). There are several ways that the state may change.
beamToNamed()
If you use the beamToNamed() you are passing in a new path which will replace the current state. Once the state has changed, the current locationBuilder of the BeamerRouterDelegate is called. Your code has to look at the fields and values of the state to choose which location should be returned for Beamer to use.
The current state is passed in to the locationBuilder and you would usually pass the same state into the BeamLocation constructor. See the code example in the previous section. You could alter or replace the state but this is unlikely.
Once the location is returned from the locationBuilder, Beamer uses that location to build the list of pages which it passes to Nav 2.0 in Flutter, Flutter builds the stack of routes, and the user sees the result of the navigation.
If the app is running in a web browser, the same process would be triggered if the user changed the location on their browser using a bookmark, forward or back command or hand edited the url. The only difference is the new path comes from the browser rather than a beamToNamed() command in the app code.
beamTo()
The process is a bit different if the beamTo() method is used. This is because it is passing in the location (BeamLocation) and that location must already have a state associated with it. Since the purpose of the locationBuilder method is to take the current state and work out which location to use, it does not need to be called when beamTo() is already passing in a location.
Whatever state is bound to the location, it will completely replace the current state. This could be exactly what you want but it might not be. There will always be a state associated with a location passed to beamTo()
context.beamTo(BooksLocation); //does not workcontext.beamTo(BooksLocation()); //does not workcontext.beamTo(BooksLocation(BeamState())); //works but has no state info//this workscontext.beamTo(BooksLocation(BeamState.fromUri(Uri.parse(‘/books’))));
A good example of using beamTo() would be for bottom navigation. You will want each tab of the bottom nav to have its own navigation stack initially showing a list but then drilling down into a details screen. If you are on the details of a book but switch to the articles tab and then switch back to books you would expect the book detail to still be showing, and not return to the book list.
If each tab is a separate location each tab can track a separate state going between list and details views. Below are a couple of code fragments that instantiate a couple of locations to be used with bottom navigation (a books tab and an articles tab) and the onTap function that uses them.
When the app starts the locations are declared with the list view path in the state. Since the locations persist but the state gets updated as the user navigates from list to detail, the last state will still be in the location when you use beamToNamed() to reinstate the location.
Update state to navigate
When you want to move from one location to another location it makes sense to use beamTo and beamToNamed, however to move between screens within a location it is more efficient to simply update the state and let the existing location rebuild the pages list as needed. The following code would be executed when a book item in a list is tapped to switch to the details screen for the book.
Push and Pop
Nav 2.0 does not use push and pop to function, however there are still times when you may use these. Things like dialogs will still be pushed onto and popped off the stack of routes just as before and will not change the state or the url if running in a browser.
You can still use Navigator.of(context).pop() to leave a screen that has been displayed with Nav 2.0, this is what happens when the automatic back button in an AppBar is tapped. If you know there is more than one route in the stack you can pop off the top item to navigate back and this will trigger and update of the state.
Recap
Let me just do a quick recap on the things that tripped me up when I was first getting to know Beamer.
pathBlueprints
If you simply used “/*” as your entire list of pathBlueprints, most of the functionality would be unchanged. The only thing the pathBlueprints really do is get some path segments beginning with colons mapped into the pathParamters. pathParamters are just a convenience that helps you write the code to build the list of pages. If there is no matching blueprint the state.pathBlueprintSegments will simply contain the same list of segments found in state.uri.pathSegments. Beamer constructs the url to show in the browser location bar from the values in the state.uri.pathSegments and does not use the pathParameters or pathBlueprints to do this task.
Only the BeamerLocationBuilder locationBuilder uses the pathBlueprints to choose which location to load, but normally your own locationBuilder method is the thing doing that task.
locationBuilder
Your locationBuilder callback chooses which BeamLocation to use based on your logic. Beamer does not do this for you. The location builder is only called if the state is changed via BeamToNamed() or the browser url is changed by the user or browser. If you are using beamTo() you are providing a location already and locationBuilder is not called. If you update the current state, this will rebuild the pages list of the current location and will not call the locationBuilder to move you to a different location (even if the new state is meant for a different location, use beamTo() instead).
It is also worth noting that any incoming state passed to locationBuilder will have been parsed with the pathBlueprints of the current location. If the state change is going to result in a different location then probably no pathBlueprints in the old location will have matched the new path. Therefore you should not rely on any state.pathParameters values being available until after the new location is in place.
BeamLocation are bound to a state
You cannot have a BeamLocation without a state. The state drives how the location builds it’s list of pages. Most of the time the locationBuilder will choose a location based on the state and then instantiate the BeamLocation passing in the current state. When you use beamTo() you will need to have a state with the location you pass in.
There is a BeamState and a BeamerState
There are two similarly named classes BeamState which is used in the locations and BeamerState which is used internally for the Beamer object. You will probably never need to do anything with the BeamerState object.
Finally
I hope this has not been too rambling and there have been some crumbs here which have given you light bulb moments about how navigation works. I have found it useful to write in order to really clarify my own understanding of the new navigator and the Beamer package.
I’d like to thank Sandro Lovnički and the other contributors for writing Beamer. This is a great package which really simplifies the process of implementing Navigation 2.0 in a Flutter app.