Instagram-like navigation with Router API in Flutter

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

--

We have seen many tutorials on Router API that only fit small apps. So I came up with a bit different approach.

In this series, we will create a well-decomposed SOLID Flutter app that can easily be scaled to dozens of screens. It has multiple tabs with a separate navigation stack in each one, full URL-and-app synchronization for web with Router API, back button support in both web and Android. It can even receive the result from a modal dialog that was re-created when that dialog’s URL was typed in.

This is a series of articles.

  1. In this step, we will reproduce the basic book app navigation for mobile.
  2. In the next step, we will add URLs and web support.
  3. In step 3, we will add multiple page stacks.
  4. In step 4, we will add a screen that awaits the result of another screen, and this awaiting will persist through app reloads.

For this solution, I made a package named app_state.

Code examples for this series can be found here.

Bare minimal app

For your convenience, examples include 1_min project with only a single screen. It is not that interesting as it has no navigation, but it can give you some sense of the package if you need something even simpler than the two-screens app below.

app_state architecture

Take a look at the example project found here. This simple app shows a list of books on its home screen, and it shows a book details screen when the list item is tapped.

The app consists of two screens. The details screen opens when an item is tapped in the main screen.

Here is the file structure:

Each screen may contain the following artifacts:

  • PageBloc. This is the heart of a page during its life cycle. If your screen was a StatefulWidget in the traditional architecture, then PageBloc plays the role of its State. It contains everything to preserve so the screen itself can be a stateless widget. However, PageBloc is richer than a State. It can produce a stream of output as the purpose of BLoC suggests. It is also aware of page stacking and can handle events related to it.
  • Screen. Most often this is just a stateless widget with PageBloc as the single argument.
  • Page. Navigator in Flutter accepts the list of Page objects to maintain the stack of routes that are displayed. So Page is a necessary adapter for the pair of bloc and screen to show in the app.

That goes for each route in the stack. So if the user is at the item screen, the stack contains 2 Page objects, each having a screen and possibly PageBloc (simple pages may go without it).

This is how these objects work together for each page:

Here is main.dart:

At the heart of the app is PageStackBloc from app_state package. In this example, it is where the entire app state is stored, because the page stack is the only state we have. In complex apps, we may have multiple page stacks, UI language, and much more, and we aggregate that into a custom app state class. But now a single global PageStackBloc will do.

A page stack cannot be empty (otherwise, what to show on the screen?), so we initialize it with bottomPage that may never be changed later on. This is where BookListPage is created.

The rest of this file should look familiar if you have approached Flutter Router API.

RouterDelegate is the most important object here from Flutter’s perspective. It builds the root widget of the app. If you have worked with Router API directly, you had created your own delegate that most often builds the root Navigator with that famous declarative list of pages.

Here we use PageStackRouterDelegate from app_state package. It builds a Navigator with pages from the stack.

Finally, there is PageStackBackButtonDispatcher. It handles Android back button and pops a page from the given stack.

Next, BookListPage:

Pages are always simple. They only pass data to the superclass:

  • key that Flutter uses when diffing two sets of pages to check if it needs to create or dispose some.
  • bloc, a PageBloc. When a page is created, the bloc is created with it and so persists until the page is popped and disposed.
  • createScreen, a factory to create the screen widget with the given bloc.

Pages are typed. The first type parameter, void here, is what is returned on pop.

Next, the bloc:

It does not have the traditional stream of states that the bloc pattern implies, but it’s only due to simplicity of this step.

It has showDetails method that the screen calls to show a book details screen. It creates a page and pushes it to the stack. This is how navigation happens with app_state package. PageStackBloc then fires a change that updates the Navigator (the one created for us in PageStackRouterDelegate) with a new set of pages.

Note that BuildContext is not used for navigation, so no more ‘BuildContext plague’. This is because the app knows the exact navigator to update and does not have to look it up with a BuildContext object.

The architectural decision to show screens from blocs may sound questionable. After all, ‘Lo’ in BLoC stands for logic, and screens are presentation, not the logic. To understand the idea, consider page blocs to be a decomposition of the app state that Router API introduced, and not pure logic state machines. If you need blocs that know nothing about screens, you may still have this level of architecture too.

The screen itself is very simple. The only notable thing is calling bloc.showDetails:

Next, the book details page:

It is so simple that it does not need a bloc. For that, we use StatelessMaterialPage of app_state. It only has the screen widget which in turn has nothing notable.

You may now run this project in Android or iOS and test the navigation. However, if you run it in web, you will see that it obviously lacks URL support:

Follow to the next step where we add it.

--

--

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