Flutter for Single-Page Scrollable Websites with Navigator 2.0 — Part 5: Scroll To Index

Cagatay Ulusoy
6 min readJul 18, 2021

In the fourth part of this series, we explored implementing a single-page scrollable website (SPSW) whose sections are built at once and have varying, unpredictable heights before being laid out. In this article, we will build the same app but this time the sections will be built lazily using the Scrollable Positioned List library by google.dev.

If you haven’t read the previous articles, I would strongly suggest starting from the first part of this series since the implementation details of the common widgets will not be mentioned in this part.

google.dev is a verified package owner which has open source packages published by Google developers. These packages are often written by Googlers who are not on the Dart and Flutter teams and their daily jobs do not include maintaining these packages. Although their contribution to the community is high quality and highly appreciated, we should not expect frequently updated and well-maintained packages. In fact, when I use the current stable version (0.1.0) in this sample app, I receive Unsupported operation: Infinity or Nan toInt exception which was reported 10 months ago and still was not fixed as of today, although there is an open PR. I forked this repository and applied the fix on my branch to be able to use this package in this sample app.

EDIT: After 8 months, this library received an update which finally supported null-safety in stable version and also fixed the bug mentioned.

ScrollablePositionedList

ScrollablePositionedList is an index-based scrollable widget. It reports the current position and scrolls to a target position with the list item index numbers instead of logical pixel numbers. We use this widget to solve the problem of not being able to calculate the target section’s offset when we build the sections lazily. Before using ScrollablePositionedList we should understand how it is implemented so that we are aware of its limitations.

ScrollView widgets have cache area. The list items that fall in the viewport’s cache area are already laid out no matter they are visible or not. If the target list item to be scrolled is in the cache area, the ScrollablePositionedList can scroll to the target index directly since the items are already laid out.

If the target index is not in the cache area, ScrollablePositionedList simulates a scroll experience like an illusionist. As it starts scrolling to the target list item index, it creates a new list in addition to the existing list. Both lists scroll at the same speed. As the position gets closer to the target list item, the initial list fades out, and the new list fades in. When the scrolling stops at the target list item, the scroll offset in the new list is equal to 0 pixels.

We have the following problems with ScrollablePositionedList :

  1. It reports the current position with index numbers instead of logical pixels. Hence, we can’t determine the leading index as we did in the previous sample apps.
  2. ScrollablePositionedList widget doesn’t expose a ScrollController . As a result, it is tricky to get the correct scroll offset and it will not work with Slivers, and Scrollbar.
  3. It may internally consume the ScrollNotification coming from its internal list. We need to revise how we distinguish between the user scroll events and scroll events generated programmatically.

In this article, we will explain how to solve these issues.

ColorSections

As in the previous sample app, we provide colors,colorCodeNotifier , and shapeBorderTypeNotifier to this widget.

  • colorCodeNotifier is listened for the color code updates coming from different sources except the scrolling.
  • colorCodeNotifier value is set when the first visible section (trailingIndex ) is changed or a shaped button is clicked.
  • shapeBorderTypeNotifier value is set when a shaped button is clicked.

Listen to Color Code Update

As mentioned in the introduction part of this series ScrollablePositionedList isn’t a regular ScrollView . It has a custom implementation for controlling the jumps and scrolling to a particular list item index: ItemScrollController .

We will use scrollTo method for scrolling programmatically to a section. It corresponds to animateTo method of the ScrollController class. However, this time instead of providing the target position as a logical pixel value, we will provide the index number of the target section to the scrollTo method.

Similar to the previous article, we do the initial scrolling and start listening for the color code updates after the widget is laid out.

Finding the Trailing Index

We aim to notify the colorCodeNotifier listeners as the first visible item index (a.k.a trailingIndex) changes so that we can update the URL on the address bar and fill the background of the menu button whose text is equal to the color code in the trailingIndex .

In addition to the ItemScrollController , ScrollablePositionedList also has ItemPositionsListener which is a type ValueListenable<Iterable<ItemPosition>> . It provides the list of partially or fully visible items in the viewport.

When we receive a user scroll notification, we will determine the trailingIndex by iterating through the ItemPositions and filter the ones whose itemTrailingEdge is larger than 0 . The one that has the minimum itemTrailingEdge value is the first visible index.

itemTrailingEdge: Distance in proportion of the viewport’s main axis length from the leading edge of the viewport to the trailing edge of the item.

Receiving the UserScrollNotification

As mentioned before, ScrollablePositionedList consumes the ScrollNotification internally and may prevent its bubbling which is very unlucky because we have been utilizing these notifications in the previous sample apps to detect user scroll events.

According to the source code, ScrollablePositionedList uses primary and secondary PositionedList widget which extends theCustomScrollView widget. If the target list item is not in the cache area then it will lay out the secondary list widget, and there will be a fading transition between the primary and the secondary list widgets.

The secondary PositionedList has a parent NotificationListener whose onNotification callback property returns false . This means that the scroll events generated on the secondary PositionedList will bubble up to the ancestors.

The primary PositionedList also has a parent NotificationListener . In this one, onNotification callback property returns _isTransitioning bool. The scroll events generated on the primary PositionedList will be canceled if only there is transitioning to the secondary PositionedList . In other words, if the target list item is on the cache area, the scroll event notifications will bubble up to the ancestors. Otherwise, they will be consumed and bubbling will be canceled inside the ScrollablePositionedList widget.

scrollable_positioned_list-L324-L394

Our ultimate goal is receiving the UserScrollNotification so that we can determine the trailing index and notify the colorCodeNotifier listeners. Luckily, ScrollablePositionedList will not cancel the UserScrollNotification bubbling because when a user is scrolling, it will not need to transition to the secondary PositionedList . The transition happens only when we scroll programmatically to an item that is not in the cache. Hence, we can wrap the ScrollablePositionedList with NotificationListener , receive the UserScrollNotification and update the listeners of the colorCodeNotifier with the hex color code value of the trailing item.

Here is the full code for the ColorSections widget:

Resizing the Browser Window

We have similar behavior as in the previous sample app. When the browser window is resized, the app may show the wrong section, however, it gets fixed on the next user scroll events or button click.

Conclusion

In this article, we learned how to lazily build sections that have varying and unpredictable sizes in a scrollable widget. This was the last article regarding the scrolling widget options for our sample apps. In the next article, we will start discussing the navigation implementation.

You can run this sample app by right-clicking on the main_003.04.dart file in the project source code. If you liked this article, please press the clap button, and star the Github repository of the sample apps.

--

--