Flutter for Single-Page Scrollable Websites with Navigator 2.0 — Part 6: Navigation
In the previous articles, we focused on implementing a single-page scrollable website from the scroll logic point. In this and the next article, we will explore how to utilize the Navigator 2.0 API to manage the URLs on the browser’s address bar.
- Part 1: Introduction
- Part 2: Scroll to Position
- Part 3: Scroll to Page
- Part 4: Ensure Visible
- Part 5: Scroll to Index
- Part 6: Navigation
- Part 7: Query Params
If you are not familiar with the fundamentals of declarative navigation, I would suggest taking a break here and starting with my Flutter Navigator 2.0 for Authentication and Bootstrapping series. In the Web part of that series, we covered the two-way
RouteInformation flow between the
Router widget and the operating system (OS). For the sake of simplicity, we didn’t dive into how the Flutter framework layer communicates with the OS.
- Flutter framework layer contains platform, layout, and foundational high-level libraries written in the Dart language. Although it is the layer that developers mostly code in, accessing the lower layers when needed is possible.
- Flutter engine layer is a portable runtime for hosting Flutter applications. It is the lowest layer of Flutter. It provides the low-level implementation of Flutter’s core API. It is mostly written in C++ but also Java, Objective-C, and Dart to provide communication with the underlying OS.
- The embedder layer is a native OS application. It is an entry point to the underlying OS and contains platform-specific dependencies. It hosts the entire Flutter content and also enables integrating Flutter code as a module to the existing native applications.
For Web apps, it is actually not the OS that the Flutter app communicates with. A Web app is sandboxed in the Web browser application so it can’t directly access the file system or low-level network . For example, we can’t import
dart.io libraries to Flutter Web projects. However, this doesn’t make any difference for the Navigator 2.0 API, because in the end, it is implemented in the Flutter framework layer, and it doesn’t need to know the engine implementation details.
When we are working with the Navigator 2.0 API for Web apps, we quite often hear the term browser history. The first thing we should know is that the browser history is different than the navigation history (a.k.a navigation stack) managed by the
Navigator widget. While the
BrowserHistory is included in the Flutter Web Engine, the
Navigator widget is part of the Flutter framework layer.
Navigator widget takes a list of
Page instances to construct a navigation history. The
pages are then turned into a stack of
Route s. When the back button is dispatched in the mobile platforms, the
maybePop method of the
Navigator widget will be invoked to pop the current route.
BrowserHistory is a list of history entries each contains a location (URL) and a state object. When we click the browser’s backward or the forward buttons, the browser takes the information from the entry list prior to or next to the current history entry in the
BrowserHistory class. Then, the
Router widget in the Flutter framework maps the
BrowserHistory entry to the
RouteInformation instance to build a new navigation history with the help of its delegates.
In the last four sample apps of this series, we manage the entries in the browser history and update the URL in the browser’s address bar with the following cases:
- First visible section changes (trailing index) as the user scrolls.
- Top or side navigation menu button clicks.
- User interaction with the Web browser app such as back/forward button click, or entering URL on the address bar.
- Clicking the shaped button in the sections.
- Clicking the barrier of the shaped border dialog.
SinglePageAppConfiguration is a custom data type that is used by the
Router widget and its delegates in the following ways:
RouteInformationProvidergets location and state information from the
BrowserHistoryentry and maps it to the
RouteInformation. Then the
RouteInformationand returns an instance of the configuration. Finally, the
RouterDelegateupdates the app state according to this configuration instance and builds provides a
Navigatorwidget to the
RouteInformationin certain events to the engine. For example, once the navigation stack changes upon app state update, a new
RouteInformationreporting is scheduled. When the
RouteInformationreporting task is scheduled, the
Routerwidget first retrieves the current app configuration from the
RouterDelegateand then passes it to the
RouteInformationParser. Using this configuration,
RouteInformationwhich is then provided back to the engine through the
In my opinion, defining a proper app configuration is the key to implement a good navigation logic using the Navigator 2.0 API. In our case, the app state is defined by three properties of the
shapeBorderType , and
colorCode field is set in the following ways:
- The user clicks a button on the side or top navigation menu bar.
- The first visible section changes.
- The user specifies a color code by typing a URL on the address bar.
- The user clicks a shaped border button that has an associated color.
shapeBorderType field is set when the user clicks a shaped button in any section or the user specifies the type by typing a URL on the address bar.
unknown field is set depending on the validity of the URL typed by the user on the address bar.
Since this is a single-page website, the content of the
HomeScreen is expected to be heavy, so we should avoid re-instantiating the entire
HomeScreen each time the
Navigator widget is rebuilt. Hence, we create the
HomePage instance when the
RouterDelegate is instantiated, and use this instance when building a
In the sample apps, I could have used a customized
HomePage class which extends the
Page class. However, I realized that each time the
Navigator widget is built, the builder method of the
HomePage returned a new
HomeScreen widget instance although the
Page key was the same. This is probably the expected behavior but it causes a really bad user experience especially in the case that list items are not built lazily. This issue is demonstrated in the below screen recording. 👇
Instead of using a custom
Page class for the
HomePage , I instantiated a
MaterialPageRoute when the
RouterDelegate is instantiated, and used that instance when building the
Navigator widget. As can be noticed in the below recording, unlike the previous case, the
HomeScreen is not constructed each time the
Navigator widget is built. Hence, the scrolling when the first visible section changes is smoother .`👇
ShapePage is a very simple Cupertino dialog. It is shown when any of the shaped color buttons is clicked. One way of showing this dialog is by calling the
showCupertioneDialog() method of the Flutter API on button click.
Let’s see the implementation of
showCupertioneDialog() in Flutter:
The modal barrier is a barricade between the current route and the route below it in the navigation stack. It is usually semi-transparent and you almost feel like you can interact with the widget in the below route. The barrier’s task is preventing this interaction. When
barrierDismissable is set
true, the clicks on the barrier invokes
Navigator.pop method, meaning that the
CupertinoDialogRoute that has the
ShapeDialog widget will be popped.
showCupertioneDialog method which pushes a new route to the
Navigator widget is so imperative but we want the navigation to be declarative and separate the concerns. The
ShapedButton should not know anything about the navigation logic on click events. It is the job of the
RouterDelegate to build a
Navigator widget on app state changes caused by user interactions.
Let’s do this in a declarative way. We will first define a custom
Page class. When the
Navigator widget is built and if this page is included in the
pages list depending on the app state, a
CupertinoDialogRoute will be created.
Router widget receives a new
RouteInformation from the
RouteInformationProvider , it calls the
parseRouteInformation method of
RouteInformationParser . Here we will map the
RouteInformation to the
SinglePageAppConfiguration according to the following logic:
- If the URL doesn’t include a path, then we return a configuration that is constructed with
- If the URL includes 2 path segments, the first path segment should be
colors, and the second path segment should be a valid hex color code. In this case, we return
homeconfiguration. For example: `https://mysinglepageapp.com/colors/ff5722`.
- If the URL includes 3 path segments, the rules for the first two segments apply the same. The last path segment should be a valid shape border type. In this case, we return
shapeBorderconfiguration. For example: `https://mysinglepageapp.com/colors/ff5722/rounded`.
- In any other cases, we construct and return the
Router widget schedules route reporting, it calls the
restoreRouteInformation method of the
RouteInformationParser to forward a
RouteInformation to the engine according to the current app configuration.
The main responsibility of the
RouterDelegate is building a
Navigator widget when the
Router widget asks for it as a result of an app state change or system event. It also manages the configuration so that
Router widget uses it whenever
RouteInformation reporting task is scheduled.
Navigator widget is usually located near the top of the widget tree. The widget that causes app state change on user interaction may be deep down in the widget tree. We need to find a good mechanism to propagate the state change from the deep down of the tree to the top of the widget.
In the Flutter Navigator 2.0 for Authentication and Bootstrapping series, we passed callback functions starting from the
RouterDelegate to the children widgets through the
Page classes. This is not recommended because as the app gets more complex, we will need to pass the callback methods on many widgets from the top of the tree until the destination widget.
There are different ways of improving the app state management using the state management libraries. In this article, we will not focus on state management topic but we will use
ValueNotifier instances instead of callback methods. Using
ValueNotifier will not solve the problem of passing the
ValueNotifier instances down to the children in the widget tree but by using
ValueNotifier we make sure that its listeners will be invoked only when the single value that it holds is updated.
ValueNotifier A ChangeNotifier that holds a single value. When value is replaced with something that is not equal to the old value as evaluated by the equality operator ==, this class notifies its listeners .
Reacting to App State changes
Router widget is notified as a result of app state change, it expects a
Navigator widget from the
RouterDelegate . In the
build method of
RouterDelegate we will return the
Navigator widget with a list of
The logic is very simple:
unknownis set to true, the stack shows only the
unknownis set to false, in any case, the bottom page in the stack is
- If the selected shape border type and selected color both have values, we add the
HomePagein the stack.
After receiving the
Navigator widget, the
Router widget may schedule route information reporting and ask the
RouteInformationParser to restore the
RouteInformation which helps to update the URL in the address bar of the Web browser.
When we click the barrier field of the dialog route,
onPopPage method of the
Navigator widget is triggered. In this case, we first make sure that route is actually popped. Then, we check the route’s name. If the name equals to the
ShapePage`s name, we set the
shapeBorderTypeNotifier value to
Here, I handled setting the
shapeBorderTypeNotifier value to
onPopPage method of the
Navigator widget. I am not happy with doing things in
onPopPage method because it still sounds legacy. It could have been better to update the app state on user interaction events such as
onDialogDismissed . However, as of today, I am not aware of a callback that is fired when the dialog is dismissed due to a barrier. Hence, I had to handle the app state change in this way.
Reacting to URL updates
After receiving the configuration from the
RouteInformationParser , the
Router widget passes it to the
RouterDelegate by calling its
setNewRoutePath method. In this method, we have a chance to update the values that are held in the
ValueNotifier fields. Then, the
Router calls the
build method of the
RouterDelegate and receives a freshly constructed
Navigator widget according to the new state.
Reporting the Configuration
Router schedules reporting the
RouteInformation to the engine, it retrieves the current app configuration from the
RouterDelegate . Therefore, in the getter method of the
currentConfiguration we need to construct a
SinglePageAppConfiguration according to the values that are held in the
In this article, we studied the integration of the Navigator 2.0 API into our sample apps. From the first sample to the fourth sample app, we use selected color and shape border types as variables in path segments for URLs. To see the implementation in the demo you can run any of these samples on Github. In the next article, we will explore using these variables as query segments in one path segment
If you liked this article, please press the clap button, and star the Github repository of the sample apps.