Flutter Lesson 21 of 83 3 min read
Scrolling, SingleChildScrollView and Slivers in Flutter
Make screens scroll in Flutter with SingleChildScrollView and CustomScrollView. Learn slivers, collapsing app bars and pull to refresh.
On this page
Content that does not fit the screen needs to scroll. Flutter has three tools, for three situations.
| Situation | Use |
|---|---|
| A fixed layout that might be too tall | SingleChildScrollView |
| A long list or grid of similar items | ListView, GridView |
| Several different sections scrolling together | CustomScrollView with slivers |
SingleChildScrollView #
Wrap a Column in it and the overflow error disappears. The most common use is a form, which must scroll when the keyboard opens.
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Sign up')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final label in ['Name', 'Email', 'Phone', 'City', 'Password'])
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: TextField(
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
),
),
FilledButton(onPressed: () {}, child: const Text('Create account')),
],
),
),
),
),
);
}
It builds everything inside it at once, so do not use it for long lists.
Inside a scroll view, Expanded and Spacer do not work along the scroll direction, because the available height is unlimited.
The problem slivers solve #
Suppose a page has a header, a horizontal row of categories, a grid of products and then a list of reviews, all scrolling as one. Nesting a ListView inside a SingleChildScrollView either throws an error or forces shrinkWrap, which builds every item.
A CustomScrollView takes a list of slivers: pieces of scrollable content that are built lazily and scroll together as a single surface.
CustomScrollView #
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: CustomScrollView(
slivers: [
const SliverAppBar.large(title: Text('Shop')),
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Popular', style: TextStyle(fontSize: 20)),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemCount: 6,
itemBuilder: (context, i) => Card(child: Center(child: Text('Product $i'))),
),
),
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Reviews', style: TextStyle(fontSize: 20)),
),
),
SliverList.builder(
itemCount: 30,
itemBuilder: (context, i) => ListTile(
leading: const Icon(Icons.person),
title: Text('Review $i'),
),
),
],
),
),
),
);
}
The slivers you will use #
| Sliver | Equivalent of |
|---|---|
SliverList.builder, SliverList.separated | ListView.builder |
SliverGrid.builder | GridView.builder |
SliverToBoxAdapter | Wraps one ordinary widget |
SliverPadding | Padding |
SliverAppBar | An app bar that scrolls, collapses or floats |
SliverFillRemaining | Fills the rest of the screen, for empty states |
Only slivers may go in the slivers list. Wrap an ordinary widget in SliverToBoxAdapter.
SliverAppBar #
| Property | Behaviour |
|---|---|
pinned: true | The bar stays visible at the top when collapsed |
floating: true | The bar reappears as soon as you scroll up |
snap: true | With floating, snaps fully in or out |
expandedHeight | Height when fully expanded |
flexibleSpace | Content that collapses, such as a cover image |
SliverAppBar.medium and SliverAppBar.large give the Material 3 collapsing titles.
Pull to refresh #
Wrap any scrollable in RefreshIndicator.
RefreshIndicator(
onRefresh: () async {
await _reload(); // the spinner stays until this future completes
},
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, i) => ListTile(title: Text(_items[i])),
),
)
If the list can be short or empty, give it physics: const AlwaysScrollableScrollPhysics() so it can still be pulled.
Scroll behaviour #
ListView(
physics: const BouncingScrollPhysics(), // iOS-style bounce
// physics: const ClampingScrollPhysics(), // Android-style stop
// physics: const NeverScrollableScrollPhysics(), // disable scrolling
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: const [],
)
By default Flutter uses the right physics for each platform.
Showing a scrollbar #
Scrollbar(
child: ListView.builder(itemCount: 100, itemBuilder: (c, i) => Text('Row $i')),
)
On desktop and web a scrollbar appears automatically.
Try it yourself #
Build a profile page with a CustomScrollView: a SliverAppBar with an expandedHeight of 200 and a cover image in flexibleSpace, a SliverToBoxAdapter with a short bio, and a SliverGrid of photos.