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. If you see this crash in your app, you can add my repo to pubspec file in your project:
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
- 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.
ScrollablePositionedListwidget 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
- It may internally consume the
ScrollNotificationcoming 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.
As in the previous sample app, we provide
colorCodeNotifier , and
shapeBorderTypeNotifier to this widget.
colorCodeNotifieris listened for the color code updates coming from different sources except the scrolling.
colorCodeNotifiervalue is set when the first visible section (
trailingIndex) is changed or a shaped button is clicked.
shapeBorderTypeNotifiervalue 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:
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
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
In addition to the
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 the
CustomScrollView 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.
PositionedList has a parent
onNotification callback property returns
false . This means that the scroll events generated on the secondary
PositionedList will bubble up to the ancestors.
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
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
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
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.
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.