FLUTTER

It's a deep dive — Slivers under the hood — Build complex scrolling behaviour with Slivers in Flutter

Slivers API has always been something that I was scared of using. Even before understanding it.

Free Link

And that happens with the best of us. And if you are, too, and you want to learn Slivers once and for all, and build apps that are smooth-scrolling and have complex scrolling behaviour, you once thought of building, you would want to keep reading.

There are a lot of articles and videos about Slivers, but a lot of it is scattered.

And sometimes we just keep pushing the learning till the time we need it. Because either it is boring or too advanced.

So this is one place you can come to for either a brush-up or an in-depth dive into Slivers. Or if you want to meditate. You choose.

Let's see what we are going to cover in this article.

Contents

  1. ListViews and their horror
  2. ShrinkWrap, the Loki to our Marvel
  3. Viewport, a look into the wild
  4. Slivers, our saviour
  5. Part 2— Let's Talk About Slivers in Flutter While Building a Contacts Application (Coming Soon)
  6. Part 3— Let's Talk Sliver Protocol and a Deep Dive into Slivers (Coming Soon)

What Will We Cover from the Sliver World

  • CustomScrollView
  • SliverList
  • SliverToBoxAdapter
  • NestedScrollView
  • SliverMainAxisGroup
  • RenderSliver
  • SliverConstraints
  • SliverGeometry
  • SliverProtocol

Comment below if you'd like this to be a full hands-on Slivers series. We could build real and complex apps and explain every part in detail?

Not that it is not already a series, but if you want me to build more custom scrolling experience, or something you would like to learn, comment below.

Take a deep breath before we start: Source
Take a deep breath before we start — Illustration Credits: Andrew Rae

ListViews and their horror

One of the first widgets that every Flutter Developer uses is ListView.

We all have used it a lot, and it works out of the box most of the time.

But we often come into a situation when we want a ListView inside a ListView or a GridView inside a ListView. Or some other combination of a Scrollable inside another Scrollable.

None
A complex design like Instagram: Source

And while building something like this, you must have seen an error like this:

Performing hot reload...
Syncing files to device Android SDK built for x86...
I/flutter (12007): ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
I/flutter (12007): The following assertion was thrown during performResize():
I/flutter (12007): Vertical viewport was given unbounded height.
I/flutter (12007): Viewports expand in the scrolling direction to fill their container.In this case, a vertical
I/flutter (12007): viewport was given an unlimited amount of vertical space in which to expand. This situation
I/flutter (12007): typically happens when a scrollable widget is nested inside another scrollable widget.
I/flutter (12007): If this widget is always nested in a scrollable widget there is no need to use a viewport because
I/flutter (12007): there will always be enough vertical space for the children. In this case, consider using a Column
I/flutter (12007): instead. Otherwise, consider using the "shrinkWrap" property (or a ShrinkWrappingViewport) to size
I/flutter (12007): the height of the viewport to the sum of the heights of its children.
I/flutter (12007): 
I/flutter (12007): When the exception was thrown, this was the stack:
I/flutter (12007): #0      RenderViewport.performResize.<anonymous closure> (package:flutter/src/rendering/viewport.dart:1129:15)
I/flutter (12007): #1      RenderViewport.performResize (package:flutter/src/rendering/viewport.dart:1182:6)
I/flutter (12007): #2      RenderObject.layout (package:flutter/src/rendering/object.dart:1619:9)
I/flutter (12007): #3      _RenderProxyBox&RenderBox&RenderObjectWithChildMixin&RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:104:13)
I/flutter (12007): #37     _invoke (dart:ui/hooks.dart:154:13)
I/flutter (12007): #38     _drawFrame (dart:ui/hooks.dart:143:3)

Now, why does this happen?

Let's break this down — and I'll make sure we are on the same page as we go.

Layout is top-down

None

When Flutter lays out your widget tree:

The parent tells each child, "Hey, you can have this much space."

The child then says: "Thanks! I'll use this much."

ListView wants infinite height

An inner ListView (or any scrollable) says:

"I'm scrollable, so I'll take infinite height, please."

But the parent already scrolls and can't allow infinite height

The outer ListView or ScrollView is already scrollable, and it can't give its children infinite height, because that would break layout logic:

  • It expects its children to have a definite size.
  • But the inner ListView says, "I don't know, I scroll! I might have a gazillion children"

This creates a contradiction, so Flutter throws the error:

"Vertical viewport was given unbounded height"

So, how do we address this issue?

We assign a definite size to the inner list, allowing the outer one to handle the layout properly. This can be done via:

  • SizedBox
  • Or setting shrinkWrap: true to a Scrollable like ListView

ShrinkWrap, the Loki to our Marvel

You might have set shrinkWrap to true in your list many times without understanding what it is.

It solved your problem. So you'd let it go. But that's where the performance takes a hit. Let's understand why.

When you set shrinkWrap: true on something like ListView, you're saying:

"I don't want this ListView to take infinite height. I want it to only take as much space as its children need."

That sounds great — but to calculate that height, Flutter has to:

  • Build every child
  • Measure their heights
  • Sum them up
  • Then set the final height

So it's like:

"I need to know everyone's exact size right now."

That's costly for large lists because you're not getting the benefit of Flutter's lazy rendering.

So this might be a solution for small lists, but generally, you wouldn't want that.

ViewPort, a look into the wild

None
Photo by Nebular on Unsplash

To understand slivers, we need to go back a little to Viewports.

A viewport is the part of the screen that is currently visible in a scrollable area. You can think of it as a window into a larger list of content.

When you scroll through a list, you're not moving all the widgets. Instead, the viewport changes what part of the content it shows.

The content itself can be much taller or wider than the screen, but the viewport defines the visible slice.

Whenever you use scrollable widgets like ListView, GridView, or CustomScrollView, Flutter automatically inserts a Viewport widget inside the widget tree, behind the scenes.

ListView(
  children: [
    Text('A'),
    Text('B'),
    ...
  ]
)

Internally, Flutter builds a widget tree that includes:

- ListView
  - CustomScrollView
    - Viewport
      - SliverList

You won't see the Viewport unless you look into the render tree or debug layout. But it is always there, managing the logic of:

  • Which part of the sliver content is visible
  • How to layout and scroll that content
  • Where to stop rendering because the content is out of view

The Viewport is what makes Slivers efficient.

Without it, Flutter would have to build all list items, even the ones not on screen.

But thanks to the viewport, only what's visible (or nearly visible) gets built and rendered.

Slivers, our saviour

You need to hear it from me first, if you haven't otherwise.

Every scrollable in Flutter uses Sliver under the hood. You may be using slivers even when you don't know it.

None

Before we go into any more details about slivers, let us revisit our original problem.

When we try this:

ListView(
  children: [
    ListView(
      children: [...],
    ),
  ],
)

We hit this error:

"Vertical viewport was given unbounded height."

This happens because:

  • The outer ListView wants to take up the entire scrollable space.
  • The inner ListView also wants to scroll, but it's inside a widget with no fixed height.
  • Flutter can't figure out how tall the inner ListView should be.

So we're nesting scrollables without telling Flutter how much space to give to each. This breaks the layout.

The Solution: Slivers

At their core:

Slivers are portions (or slices) of a scrollable area.

They are like modular pieces of a scroll view, which:

  • Tell the Viewport how much space they need
  • Respond to constraints (e.g., current scroll offset)
  • Efficiently render only the visible parts

If you don't get this, no worries. Just keep on reading.

How Slivers Fix the Problem

With CustomScrollView, instead of nesting scroll views, you compose one unified scroll view:

CustomScrollView(
  slivers: [
    SliverToBoxAdapter(child: SomeWidget()),
    SliverList(delegate: SliverChildBuilderDelegate(...)),
    SliverGrid(delegate: ...),
    SliverPersistentHeader(...),
  ],
)

Everything scrolls together as one unit.

There's no nesting or fighting over scroll behaviour because:

  • Each sliver cooperates with the Viewport
  • Each one knows how much space to take up
  • The Viewport drives everything using scroll constraints and layout rules

Why Slivers Work

Unlike ListView, slivers don't assume infinite space.

  • Instead, each sliver gets scroll constraints from the Viewport
  • It responds by returning its sliver geometry (like how much space it takes, what's visible, etc.)

That's why you don't get unbounded height issues. Each sliver plays its part within the scrolling context.

So it behaves as a single list view even when you think you are nesting scrollable together.

Thank you for reading till here. We've reached the end of this write-up, but not the end of the series. There's more. See you there.

Take a deep breath before we start: Source
Take a deep breath now that we've finished part 1 — Illustration Credits: Andrew Rae