How Slivers are made

Tomek Polański
Nov 24, 2019 · 4 min read

The best way to learn how to write your own Sliver is to know how existing Slivers are made.

I would split Slivers into groups by their complexity:

Single child Slivers

Slivers with one child that provide a special effect

Slivers containing multiple children

To understand Slivers, you need to understand Sliver constraints and geometry.

SliverConstraints & SliverGeometry

Usually, you are using Widgets that are based on RenderBox — to layout them on-screen, BoxConstraints are passed to RenderBox and its size is calculated.

Slivers are more complicated — RenderBox widgets do not care where there are on the screen, but slivers do. They need to know where they are in the scrollable space.

Slivers use SliverConstraints (RenderBox uses BoxConstraints) as inputs and calculate SliverGeometry (RenderBox calculates size).

Writing your first sliver: SliverToBoxAdapter

SliverToBoxAdapter simply adapts Widgets that are based on RenderBox, to a Sliver that you can use in a scroll view.

Every Sliver consists of aWidget and aRenderSliver.

For SliverToBoxAdapter, the widget part is pretty simple — the crucial part is creating RenderSliver, in this case, RenderSliverToBoxAdapter.

class SliverToBoxAdapter extends SingleChildRenderObjectWidget {  const SliverToBoxAdapter({
Key key,
Widget child,
}) : super(key: key, child: child);
@override
RenderSliverToBoxAdapter createRenderObject(BuildContext context) => RenderSliverToBoxAdapter();
}

The interesting part is RenderSliverToBoxAdapter which has only one method performLayout. This is the full source code, but let’s go thru it line by line later:

void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
final SliverConstraints constraints = this.constraints;
child.layout(constraints.asBoxConstraints(), parentUsesSize: true); final double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child!.size.width;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: childExtent);
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);

assert(paintedChildSize.isFinite);
assert(paintedChildSize >= 0.0);
geometry = SliverGeometry(
scrollExtent: childExtent,
paintExtent: paintedChildSize,
cacheExtent: cacheExtent,
maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize,
hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
setChildParentData(child!, constraints, geometry!);
}

Let’s diagnose it line by line:

@override
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}

In case there is no child, we skip all the calculations.

child.layout(constraints.asBoxConstraints(), parentUsesSize: true);

We perform layout on the child using SliverConstraints, and they are converted into BoxConstraints.

We specify that the parent uses a child’s size — if the child resizes, then the sliver itself will redo layout calculation.

double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child.size.width;
break;
case Axis.vertical:
childExtent = child.size.height;
break;
}

Depending on the orientation of the sliver, we calculate childExtent which is the space that the child should take — if the list is vertical, then it’s the child’s height.

final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: childExtent);

Here we are calculating the visible child’s size. In case the child is only 50% visible, this value will be 50% of childExtend’s.

This value is assigned to Geometry.paintExtent (interactive example)

final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);

To understand cacheExtent we need to talk about viewport caching.

Slivers (yellow), cache area (green), visible area (red)

In the image

  • The yellow space within the black frame is an area populated by all the Slivers.
  • The red space is a visible area — to see what is in the yellow space, you need to scroll.
  • The green space is the caching area.

Slivers in the yellow area are not rendered as that would be wasteful.

When scrolling the visible area (red) we need to layout and paint not visible Slivers — we need to give them enough time for the necessary calculation — therefore we start to load them a bit earlier. Whenever a Slivers enters the cache area (green), it will be laid out even if they are not yet visible.

The Geometry.cacheExtent (interactive example) property states how much of the sliver is in the cache (green) area.

geometry = SliverGeometry(
scrollExtent: childExtent,
paintExtent: paintedChildSize,
cacheExtent: cacheExtent,
maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize,
hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
setChildParentData(child, constraints, geometry);

Applies geometry to the underlying render object.

Coming up

In the next article, we are implementing more complex SliverFillRemaining.

* In RenderBox you need to return intrinsic min/max dimensions but that we can simplify the concept for the purpose of this article

Flutter Community

Articles and Stories from the Flutter Community