Instagram-like navigation with Router API in Flutter
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.
- In this step, we will reproduce the basic book app navigation for mobile.
- In the next step, we will add URLs and web support.
- In step 3, we will add multiple page stacks.
- 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.
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, thenPageBloc
plays the role of itsState
. It contains everything to preserve so the screen itself can be a stateless widget. However,PageBloc
is richer than aState
. 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 ofPage
objects to maintain the stack of routes that are displayed. SoPage
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
, aPageBloc
. 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: