Adding web URLs to Flutter app using app_state package

Alexey Inkin
Flutter Senior
Published in
6 min readApr 3, 2022

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 in Page and PagePath. When a page is recovered from the path, classFactoryKey is used to select the page to create.
  • key in Page and PagePath. For Page, this is Flutter’s built-in. When something changes in page list in Navigator, 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 in Page constructors. But before that Flutter’s diff in some cases there is a diff by app_state package. When the browser’s back button is pressed, an older stack of PagePath objects is applied to the current stack of Page objects. Based on that diff, app_state package creates, deletes or updates pages and their blocs and kicks the Navigator to do its diff right after. So PagePath has to be aware of its page’s key as well. This is why we tend to have formatKey method in page classes so it could be called from both Page and PagePath.

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.

--

--

Alexey Inkin
Flutter Senior

Google Developer Expert in Flutter. PHP, SQL, TS, Java, C++, professionally since 2003. Open for consulting & dev with my team. Telegram channel: @ainkin_com