Flutter - Supports observing NestedScrollView, with greater compatibility 😈

LinXunFeng
6 min read6 days ago

--

Overview

Time files so fast, it has been 9 months since the last article introducing the function of scrollview_observer, and now scrollview_observer has also arrived 1.21.0 version, now let’s take a look at what has been updated.

GitHub: https://github.com/fluttercandies/flutter_scrollview_observer

Function points

Support NestedScrollView

The following two marjor functions of scrollview_observer are now supported for NestedScrollView, as shown in the following figure.

Wait for the next article to introduce the steps in detail :)

Support center

As shown in the following code, in some scenarios, you may set center of CustomScrollView to implement the chat message page.

Widget _buildScrollView() {
return CustomScrollView(
center: _centerKey,
anchor: 1,
controller: scrollController,
slivers: [
_buildSliverListView(
color: Colors.redAccent,
onBuild: (ctx) {
_sliverListCtx1 = ctx;
},
),
_buildSliverListView(
color: Colors.blueGrey,
onBuild: (ctx) {
_sliverListCtx2 = ctx;
},
),
// center
SliverPadding(padding: EdgeInsets.zero, key: _centerKey),
_buildSliverListView(
color: Colors.teal,
onBuild: (ctx) {
_sliverListCtx3 = ctx;
},
),
_buildSliverListView(
color: Colors.purple,
onBuild: (ctx) {
_sliverListCtx4 = ctx;
},
),
],
);
}

But you want to use the jumpTo index function of item. Don’t worry, scrollview_observer has good compatibility and is already supported in the 1.19.1 version. And no additional steps are required.

observeIntervalForScrolling

I believe everyone still remembers that there are three timings for automatically triggering observation:

  • scrollStart
  • scrollUpdate
  • scrollEnd

Among them, scrollUpdate triggers observation too frequently. In fact, the results of many observations will not be much different. In most usage scenarios, it is not very important to us.

For this reason, in this update, a new observeIntervalForScrolling property is added to ObserverController , which is used to set the interval for triggering observation, thereby greatly reducing unnecessary observation calculations.

There are two points to note:

  • In order not to change the previous behavior, the default value is Duration.zero , so you need to adjust it by yourself. It is recommended to set it to Duration(milliseconds: 500) .
  • This property is only valid for scrollUpdate .

visibleMainAxisSize

The display size of item

As shown, each item has a fixed height of 200, and their corresponding visibleMainAxisSize in ListView.

visibleFraction

The visible fraction of item within sliver.

Calculation formula: visibleFraction = visibleMainAxisSize / paintExtent

As shown in the figure, the current visible space of SliverList is paintExtent and the visibleMainAxisSize of item20 is 30 , so the visibleFraction of item20 is 30/376 .

SliverObserveContext

Used to get BuildContext of sliver , which is very useful in scenarios of observing the sliver .

As follows CustomScrollView configures multiple sliver .

Widget _buildScrollView() {
return CustomScrollView(
controller: scrollController,
physics: const ClampingScrollPhysics(),
slivers: [
// banner
SliverPersistentHeader(...),
// any widget in the middle
SliverObserveContextToBoxAdapter(...),
// tabBar
SliverPersistentHeader(...),
// build some SliverGird
...List.generate(modelList.length, (mainIndex) {
return _buildSectionGridView(mainIndex);
}),
],
);
}

We need to observe which SliverGrid is currently the first, and then update the selected index of TabBar synchronously.

/// Record each Sliver's index and BuildContext.
Map<int, BuildContext> sliverIndexCtxMap = {};

SliverViewObserver(
controller: sliverObserverController,
sliverContexts: () => sliverIndexCtxMap.values.toList(),
child: child,
onObserveViewport: (result) {
...
},
);

But during the development process, we are likely to add another layer of Sliver to SliverGrid to add decoration, spacing, etc., while onObserveViewport only recognizes the outermost layer sliver , so here we use SliverObserveContext to wrap it and become the outermost layer. In its onObserve callback, we can get the corresponding BuildContext and record it.

Widget _buildSectionGridView(int mainIndex) {
Widget resultWidget = SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(...),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// Got the BuildContext of SliverGrid
return Container(
...
);
},
childCount: 10,
),
);
resultWidget = SliverPadding(
padding: const EdgeInsets.all(8),
sliver: resultWidget,
);
// Wrap it with SliverObserveContext at the outermost layer to obtain
// the BuildContext of the outer layer.
resultWidget = SliverObserveContext(
onObserve: (context) {
sliverIndexCtxMap[mainIndex] = context;
},
child: resultWidget,
);
return resultWidget;
}

Keep IM message position

The keep position function currently has three modes to choose from

enum ChatScrollObserverHandleMode {
/// Regular mode
/// Such as inserting or deleting messages.
normal,

/// Generative mode
/// Such as ChatGPT streaming messages.
generative,

/// Specified mode
/// You can specify the index of the reference message in this mode.
specified,
}

This update is about the specified mode. Because the original refItemRelativeIndex and refItemRelativeIndexAfterUpdate parameters can only express relative indexes, but cannot express the reference coordinate system, they are abandoned.

Added refItemIndex and refItemIndexAfterUpdate , and combined with refIndexType to better specify the reference to item .

Let’s first take a look at the type definition of refIndexType .

enum ChatScrollObserverRefIndexType {
/// relativeIndex trailing
///
/// 6 | item16 | cacheExtent
/// ----------------- -----------------
/// 5 | item15 |
/// 4 | item14 |
/// 3 | item13 | displaying
/// 2 | item12 |
/// 1 | item11 |
/// ----------------- -----------------
/// 0 | item10 | cacheExtent <---- start
///
/// leading
relativeIndexStartFromCacheExtent,

/// relativeIndex trailing
///
/// 5 | item16 | cacheExtent
/// ----------------- -----------------
/// 4 | item15 |
/// 3 | item14 |
/// 2 | item13 | displaying
/// 1 | item12 |
/// 0 | item11 | <---- start
/// ----------------- -----------------
/// -1 | item10 | cacheExtent
///
/// leading
relativeIndexStartFromDisplaying,

/// Directly specify the index of item.
itemIndex,
}

As above, there are a total of three reference modes for you to choose from.

  • relativeIndexStartFromCacheExtent : Calculate the index starting from item in the rendering area. This mode is generally used for chat message insertion, because the inserted message must be at 0 . What remains unchanged before and after the inserted message is the original latest message, and its index changes from 0 to 1 , then refItemIndex can be set to 0 , and refItemIndexAfterUpdate can be set to 1 .
  • elativeIndexStartFromCacheExtent : Calculate the index starting from item in the display area. This mode is relatively rarely used. It is usually combined with the observation function, because through the observation function, we can easily know which item are being displayed. Suppose you change content of the first item, but does not want to affect the offset of the second displayed item , then this mode is just suitable for the current scenario. Because the second displayed item remains unchanged before and after the change, refItemIndex is set to 1 , and refItemIndexAfterUpdate is also set to 1.
  • itemIndex : The easiest to understand mode among the three modes. You specify the index of item used for reference, such as item11 above has changed. If we want to maintain the position, we can use the unchanged item12 as a reference, so refItemIndex and refItemIndexAfterUpdate are both set to 12 .

Remember, no matter which reference mode you choose, one thing you need to pay attention to is that the specified reference item needs to be rendered before and after the change, so as to ensure that the function of keeping position can take effect normally!

Finally

Through the explanation of the above examples, I believe you will have a clearer understanding of the use of scrollview_observer . Open source is not easy. If you also find this package useful, please give it a Star 👍

GitHub: https://github.com/fluttercandies/flutter_scrollview_observer

Author: https://github.com/LinXunFeng

This article ends here. Thank you all for your support. See you next time! 👋

Series

--

--