Adding web URLs to Flutter app using app_state package
In the first part, we created a Flutter app using app_state
package for navigation. However, on web it lacks significant functionality:
- All the navigation is happening under the main URL.
- The browser back and forward buttons do not work. This is because for the browser whatever we are doing is just moving things around. The browser is not aware that we are mimicking navigation.
Let’s fix this.
The example project
Take a look at the example project found here. This is the structure with the new files highlighted:
Let’s walk through the changes from the previous step.
Parsing URLs into PagePath objects
When a URL is typed in, a RouteInformationParser
parses it into a PagePath
object that defines a page to create:
The base PageStackRouteInformationParser
comes from app_state
package.
This class is as a factory of PagePath
objects. Here we test for paths that the app supports and return the first one that is successfully parsed.
The new classes work together like this:
The PagePath class
Path classes can be as simple as this for the list of pages:
BookDetailsPath
is more complex because it has to parse the URL with a regular expression to extract the book ID. We will get to it soon.
The page factory
When a path is parsed, the page is created by the factory we define. So pages are now created in two cases:
- At the point of navigation, as when tapping a book in the list.
- By the factory.
Here is what changed in main.dart
, and here is the factory method we will use for that:
Note that the factory uses factoryKey
and state
to decide on the page to create. I will later explain where they come from and why they are used.
For now, note that we added classFactoryKey
static field to each page’s class to do this switch.
Making Page classes use scalars
Remember BookDetailsPage
from the non-web project? We were passing a Book
object to it. It is convenient when opening a screen from another one.
But now we need to re-create pages when visiting a URL. So each page should be described with only simple data such as scalars to not depend on pages below it. This is why we should refactor all pages to only accept scalars. Here is the new BookDetailsPage
:
Here the page is created with bookId
. It gets the book by ID and passes it to the screen. For this example, we moved the book list from BookListBloc
to a global variable bookRepository
.
In the real world, you would do an asynchronous network operation. So you probably need BookDetailsBloc
that will also accept bookId
, do the lookup and update the screen when it is loaded. However, this tutorial aims to be simple and to show how far a page can go without a bloc.
Also new here is classFactoryKey
field that we use in the page factory method in the previous section. It has nothing to do with page keys that Flutter’s Navigator
uses to compare pages in the stack. It is only used here and in the factory method.
Finally, there is formatKey
method. You remember page keys from before that go to Navigator
. We will now need to compose this key in multiple places, so this is extracted to this method.
BookDetailsPath
This path object is complex because it has to parse a URL with a regular expression:
The constructor requires bookId
and passes state
, key
, and classFactoryKey
. Now is the good time to go into differences of these keys:
classFactoryKey
inPage
andPagePath
. When a page is recovered from the path,classFactoryKey
is used to select the page to create.key
inPage
andPagePath
. ForPage
, this is Flutter’s built-in. When something changes in page list inNavigator
, Flutter diffs the new pages’ keys to the old pages’ keys. It then creates and deletes routes (a thing that shows an actual screen widget) based on that comparison. So we definitely use them inPage
constructors. But before that Flutter’s diff in some cases there is a diff byapp_state
package. When the browser’s back button is pressed, an older stack ofPagePath
objects is applied to the current stack ofPage
objects. Based on that diff,app_state
package creates, deletes or updates pages and their blocs and kicks theNavigator
to do its diff right after. SoPagePath
has to be aware of its page’skey
as well. This is why we tend to haveformatKey
method in page classes so it could be called from bothPage
andPagePath
.
state
is another important variable in the constructor of PagePath
. It is a map to store which book we are at. It has to do with the way that browser handles back and forward button navigation.
Each time the app stores an entry in browser’s navigation history, all PagePath
objects in the stack are serialized to be stored there. What gets serialized is key
, classFactoryKey
, and state
. When back or forward navigation happens, this serialized data gets deserialized to be applied to the Page
stack for diffing. And this gets deserialized not into your original PagePath
subclass but to a generic class. So the page factory cannot work on your dear path classes but only gets classFactoryKey
and state
. This type loss during deserialization is the reason for using the map.
Back to BookDetailsPath
. There is a new tryParse
method. We were calling it from our RouteInformationParser
earlier. If the URL can be parsed into book details path, we will return it. null
is returned otherwise.
There is a new defaultStackConfiguration
getter. It is called when this URL is typed in, and the whole app’s state has to be derived from this PagePath
alone. In this case, we fill the stack with two pages: the book list at the bottom, and this book’s details at the top.
Updating the Address Bar
Now that we care for URLs, we need blocs to expose what PagePath
their inner state is equivalent to.
So we add the path
getter to blocs. In our case, it’s only BookListBloc
:
This is it. If you start this project in the browser, it just works:
- When you tap the list item, a details screen opens and the URL changes.
- The in-page Flutter’s back button continues to work as it used to. It does the hierarchy navigation, so it always closes the top screen and gets you back to the list. As the side effect, the URL gets back to that of the list.
- Any change in the URL is stored as the browser’s navigation history, so the browser’s back and forward buttons take you to the previous or the next URLs in history.
One final touch could be to use /books/1
instead of /#/books/1
. For this, use url_strategy package.
In the next tutorial, we will deal with multiple tabs and an independent navigation stack in each one.