Fixing Jumping Behavior in Flutter Chat Lists with CustomScrollView Center
This article explains why a reverse‑ordered Flutter chat ListView jumps when inserting messages at both ends, analyzes the underlying Viewport‑Scrollable‑Sliver architecture, and demonstrates how configuring the center property of a CustomScrollView with multiple SliverLists eliminates the unwanted scroll offset changes.
When implementing a chat interface in Flutter, a simple ListView with reverse: true works for basic scrolling, but inserting items at both the top and bottom causes the list to jump because the underlying SliverList length changes and shifts its position within the Viewport .
The Flutter scrolling system consists of three core components: Viewport (the visible window), Scrollable (handles gestures), and RenderSliver (lays out sliver children such as SliverList ). When new items are inserted at the head, the SliverList grows, moving its start offset and causing the visible content to shift.
One naive fix is to record the current scroll offset, insert the data, calculate the offset change, and then jump back, but this results in a noticeable flash.
The proper solution leverages the center property of CustomScrollView . By placing a SliverPadding (or any sliver) with a key at the desired center position, you can split the list into two SliverList sections—one for older messages above the center and one for newer messages below—so that inserting items on either side does not affect the scroll offset.
Example using ListView.builder (original approach):
ListView.builder(
controller: scroller,
reverse: true,
itemBuilder: (context, index) {
var item = data[index];
if (item.type == "Right")
return renderRightItem(item);
else
return renderLeftItem(item);
},
itemCount: data.length,
)Improved implementation with CustomScrollView :
CustomScrollView(
controller: scroller,
reverse: true,
center: centerKey,
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
var item = newData[index];
if (item.type == "Right")
return renderRightItem(item);
else
return renderLeftItem(item);
},
childCount: newData.length,
),
),
SliverPadding(
padding: EdgeInsets.zero,
key: centerKey,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
var item = loadMoreData[index];
if (item.type == "Right")
return renderRightItem(item);
else
return renderLeftItem(item);
},
childCount: loadMoreData.length,
),
),
],
)With this configuration, inserting new messages at the top adds them before the center sliver, extending the negative scroll extent, while inserting at the bottom adds after the center, extending the positive extent; the scroll offset remains stable, eliminating the jump.
Understanding the center key and how Viewport calculates scrollOffset = 0 is essential for building robust, bidirectional chat lists in Flutter.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.