Flutter for Single-Page Scrollable Websites with Navigator 2.0 — Part 5: Scroll To Index
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.
- 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
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
:
- 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.
ScrollablePositionedList
widget doesn’t expose aScrollController
. As a result, it is tricky to get the correct scroll offset and it will not work with Slivers, andScrollbar
.- 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 ItemPosition
s 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.
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.