Flutter for Single-Page Scrollable Websites with Navigator 2.0

 Single page scrollable websites are everywhere. In this website design pattern, all the content is presented in one long-scrolling layout that contains multiple sections. Visitors can scroll or jump to a section by clicking buttons of a sticky menu. This pattern is a good fit for small content such as brochure websites, software library documentation, portfolios, and landing pages that are used to convert users [1]. Designers also love this pattern because it is simple, clean, and enables cool scroll animations.

Jetbrains Mono single-page scrollable website
Hive docs multi-page scrollable website

In this series of articles, we will explore how to build a single-page scrollable website using Flutter. We will benefit from the Navigator 2.0 API to provide a good navigation experience to the users.

You can find the source code of this series’ sample apps on my Github page. The implementation details of the sample apps will be explained in the following articles:

Since this series doesn’t cover the fundamentals of the Navigator 2.0 API, I would suggest starting with my Flutter Navigator 2.0 for Authentication and Bootstrapping series before the sixth part of this series.

We want to achieve the following goals for our single page scrollable website samples:

  1. Clicking a button in the top or side navigation menu bar will scroll the page to the corresponding section.
  2. The URL on the Web browser’s address bar should be updated according to the button clicks on the navigation menu and the first visible section as the user scrolls on the home page.
  3. When the user enters a URL on the address bar, the Web application should show the corresponding section.
  4. When the Web browser’s back and forward buttons are clicked the URL on the address bar and the first visible section should be updated correctly.
  5. If the user types an unknown path in the address bar, a page with an error text is shown to the user.

Motivation for using Navigator 2.0

Since the articles in the Flutter Navigator 2.0 for Authentication and Bootstrapping series have received great attention from the Flutter community, I was invited to make a presentation about Navigator 2.0 API at Turkey’s biggest #Flutter Festival. The talented Flutter Turkey team built a great website for the event in a short time using Flutter. The project is open-source and available on their Github page.

I was too excited for my first tech talk at a big event and wanted to share the event program with my colleagues and friends on different channels. The event was bilingual (Turkish and English) and my presentation was in English, so they had a chance to watch and support me during the presentation.

Supportive friends among the audience 💙

However, there was a slight problem. The content of the website was Turkish and the Flutter Web app didn’t use the Navigator 2.0 API. Hence, the browser’s address bar would not be updated for the different sections of the website, and I would not be able to send the direct link of the event program. It would have been awesome if the information for my presentation had been accessible with https://festival.flutterturkiye.org/program/cagatay-ulusoy URL so that my friends would not need to translate the page content to see my talk in the event program. We will achieve this user experience in the sample apps of this series.

Sample Apps

We will focus on two things in the sample apps: scrolling logic and navigation. For the main scrollable content of the website (ColorSections), we will use ListView , PageView , SingleChildScrollView + Column , and ScrollablePositionedList widgets in four sample apps. For navigation we will use the Navigator 2.0 API with two different URL structure: path variables and query parameters.

Although state management and responsive design are very important part of a single page scrollable website, these topics are out of scope in this series. We will use ValueNotifier / ValueListenable for state management not to favor any 3rd party state management libraries (Bloc, Provider, Redux etc.). From responsive design point of view, we will only make sure that the sections in the single page are not clipped and the scroll behavior is not broken after browser window resize.

We have 3 properties used to construct the app state of the sample apps:

  • color : Each section in the home page is associated with a primary color constant of Material design’s color palette. The color state contains a String hex color code and the source of the color update which can be scrolling, button click, or browser entry.
  • unknownState : If the URL entered in the Browser’s address bar is invalid then we set the unknownState to true. In this case, the app shows the Unknown page to tell the user that the URL is invalid.

In the first sample app, we will build the scrollable content using the ListView.builder constructor. Calling this method creates a ListView whose items are created lazily (on-demand). The itemBuilder parameter creates an item for a given index and it is called when the index is visible as the user scrolls onto the screen.

At first, this sounds like the perfect solution for our requirements from the performance point of view because we won’t create all the website content at once. However, we will face a problem when we intend to jump to a color section in the list. Before explaining the problem, let’s recap how we scroll to a position in a Scrollable widget.

In Flutter each Scrollable widget is controlled with a ScrollController which notifies its listeners on ScrollPosition updates. We can access the scroll offset value using ScrollPosition.pixels . When we want to programmatically scroll to an offset in a Scrollable widget, we can use jumpTo or animateTo methods with a given pixel value.

Scrollable widgets in the Flutter framework are scrolled based on pixels instead of item index. In our case, when a user presses a button in the navigation menu widget, we want to jump to the index of the selected color section in the scrollable list and this is the problem. As stated in this Github issue, when an item in the list is not visible (not laid out, not built), we won’t know the offset of the target section unless we can estimate the height of the previous items in the scroll direction.

In November 2017, the Flutter team stated in this comment that this feature is something that they want to offer. Although this sounds like a very critical requirement for many apps, as of summer 2021, we still don’t have a built-in solution in Flutter API for this use case. Apparently, the team decided that this is how the Flutter framework should work and they avoided creating a mess in the API. It doesn’t seem possible to achieve this requirement unless we build all the list items which is what we are trying to avoid by using the ListView.builder .

We can still utilize the ListView.builder if we precisely know the heights of each item in the list. In the first sample app, all the color sections in the scrollable list will have the same height. Therefore, we can calculate the scroll offset by multiplying the index of the color in the colors list with the item height and programmatically scroll to the target section.

The second sample app is very similar to the first sample app. In this sample, we will use PageView instead of ListView . PageView widget is a scrollable list whose children have the same size which is equal to the viewport size by default. Each item in the list is called a page. We can consider the PageView widget as a ListView but more tailored for items of equal size.

Using the PageController of the PageView , we can jump to the selected index. This also sounds like a good fit for the case when the design requires sections of the same height. However, in the article we will discuss the problems when the item height is required to be larger than the viewport height.

In the third sample app, we will learn how to live with the expense of laying out all the list items with various heights. We will use SingleChildScrollView which is a scrollable box usually used with a Column . To achieve the scroll requirements, we will do the following:

  1. Create a widget list containing all the color section widgets.
  2. Assign a GlobalKey to each section widget.
  3. Set the color section widget list as the children of the Column widget and provide the Column to the SingleChildScrollView as its child.
  4. When we want to scroll to an index programmatically, we will get the GlobalKey of the color section widget and provide the currentContext of the GlobalKey as a parameter to Scrollable.ensureVisible method. The ensureVisible method ensures that the widget with the given context is visible.

Note that this way of scrolling to an item is the most expensive one among all options in terms of performance. If the number of the sections are small, and the sections are not content-heavy, using SingleChildScrollView + Column could be the easiest solution.

In the previous samples, we utilized Flutter’s built-in solutions for creating a list of sections either lazily or at once. There is still a less expensive way of laying out the list items with unpredictable height on-demand (lazily) and scroll to an index thanks to the Scrollable Positioned List library by google.dev.

This library solves jumping to a section that has not yet laid out in a very clever way:

  • If the destination section index is too far from the current index (not yet laid out), the widget uses a new list in addition to the current one.
  • The scrolling is started in both lists at the same pace.
  • The newly created list fades over the old list and starts showing the items that are close to the target.
  • When the scrolling reaches the target, the newly created list is already fully visible and stops.
  • The offset of the new list is set to 0 and the old list becomes invisible.

Although this looks like the perfect solution, it is still a third-party workaround solution that comes with limitations. As of today, the ScrollablePositionedList widget doesn’t expose a ScrollController . As a result, it is tricky to get the correct scroll offset, handle the scroll notifications, and it will not work with Slivers, and Scrollbar.

In the last 4 sample apps, we use path segments with path variables in URLs:

  • constant colors text as the first path segment
  • a path variable that stands for hex color code as the second path segment
  • shape border type path variable as the third segment

`http://localhost:57155/colors/ffeb3b/circle`

In the fifth sample app, we will use query parameters in URLs with one path segment called section which contains two query parameters color and borderType .

`http://localhost:57073/section?color=ffeb3b&borderType=circle`

Conclusion

In this article we had an introduction to single page scrollable website concept, and explained the sample apps that we will be building throughout this series. In the next article, we will start with the implementation details of using the ListView widget lazily. If you liked this article, please press the clap button, and star the Github repository of the sample apps.

Follow Flutter Community on Twitter: https://Twitter.com/FlutterComm

Comments

Popular posts from this blog

A Data Science Portfolio is More Valuable than a Resume

Better File Storage in Oracle Cloud