Scrollable / Slivers
Flutter scrollable widgets: ListView, GridView, SingleChildScrollView, CustomScrollView with slivers, and the patterns for smooth scrolling.
Flutter — scrollable widgets
EXAMPLE
import 'package:flutter/material.dart';
// ===== ListView (vertical by default) =====
ListView(
children: [
for (final t in titles) ListTile(title: Text(t)),
],
);
// ListView.builder for lazy (don't build all items up front):
ListView.builder(
itemCount: items.length,
itemBuilder: (ctx, i) => ListTile(title: Text(items[i])),
);
// ListView.separated for dividers between items:
ListView.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (ctx, i) => ListTile(title: Text(items[i])),
);
// ===== GridView =====
GridView.count(crossAxisCount: 3, children: [...]);
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, mainAxisSpacing: 4, crossAxisSpacing: 4),
itemCount: photos.length,
itemBuilder: (ctx, i) => Image.network(photos[i]),
);
// Responsive grid (max width per item):
GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 220, childAspectRatio: 3/4),
itemCount: items.length,
itemBuilder: ...,
);
// ===== SingleChildScrollView (one scrollable child) =====
SingleChildScrollView(
child: Column(children: [
HeaderWidget(),
Form(...),
BigImage(),
]),
);
// Use when the child is NOT a long list. For long lists, use ListView.builder.
// ===== CustomScrollView with slivers =====
CustomScrollView(
slivers: [
const SliverAppBar(
title: Text('Feed'),
floating: true,
snap: true,
pinned: false,
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(background: Image.asset('header.jpg', fit: BoxFit.cover)),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(ctx, i) => ListTile(title: Text(items[i])),
childCount: items.length,
),
),
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
delegate: SliverChildBuilderDelegate(
(ctx, i) => Image.network(photos[i]),
childCount: photos.length,
),
),
],
);
// ===== Pull-to-refresh =====
RefreshIndicator(
onRefresh: () async { await fetchItems(); },
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: items.length,
itemBuilder: ...,
),
);
// ===== Infinite scroll =====
ListView.builder(
controller: scrollController, // listen for end-of-list
itemCount: items.length + 1,
itemBuilder: (ctx, i) {
if (i >= items.length) {
// Load more here
return const Center(child: CircularProgressIndicator());
}
return ListTile(title: Text(items[i]));
},
);
// In initState:
// scrollController.addListener(() {
// if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 200) {
// loadMore();
// }
// });
// ===== Horizontal scroll =====
ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: chips.length,
itemBuilder: (ctx, i) => Padding(padding: const EdgeInsets.symmetric(horizontal: 4), child: Chip(label: Text(chips[i]))),
);
// ===== Patterns to internalise =====
// - .builder over child lists for long content
// - CustomScrollView + slivers for mixed scrolling content
// - RefreshIndicator + AlwaysScrollableScrollPhysics
// - Listen to ScrollController for infinite loading
// ===== Pitfalls =====
// - Putting ListView inside a Column without bounded height -> layout error
// - Using SingleChildScrollView for a long list -> all items built up front
// - Forgetting to dispose ScrollController -> leaks
// - Nesting scrollables on the same axis -> gesture confusion
Why it matters
Pick the scrollable that matches the content shape. ListView.builder for lazy lists, GridView for grids, SingleChildScrollView for short forms, CustomScrollView + slivers for mixed layouts with collapsing headers. RefreshIndicator + ScrollController cover pull-to-refresh and infinite scroll.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
CustomScrollView(slivers: [
SliverAppBar(title: Text('Header'), floating: true),
SliverList.builder(
itemCount: 50,
itemBuilder: (_, i) => ListTile(title: Text('Row $i')),
),
])
Try it Yourself »
Discussion
Loading…