ListView
ListView renders a scrollable list. ListView.builder is the lazy variant — only builds widgets for items currently on screen. The right choice for any list with more than ~20 items.
Builder, separated, infinite scroll, pull-to-refresh
EXAMPLE
import 'package:flutter/material.dart';
class FeedPage extends StatefulWidget {
const FeedPage({super.key});
@override
State<FeedPage> createState() => _FeedPageState();
}
class _FeedPageState extends State<FeedPage> {
final _posts = <Post>[];
final _scroll = ScrollController();
bool _loading = false;
bool _hasMore = true;
@override
void initState() {
super.initState();
_loadMore();
_scroll.addListener(() {
if (_scroll.position.pixels > _scroll.position.maxScrollExtent - 200 && !_loading) {
_loadMore();
}
});
}
Future<void> _loadMore() async {
if (!_hasMore || _loading) return;
setState(() => _loading = true);
final batch = await api.fetchPosts(after: _posts.lastOrNull?.id);
setState(() {
_posts.addAll(batch);
_hasMore = batch.length == 20;
_loading = false;
});
}
@override
Widget build(BuildContext context) {
// 1) Pull-to-refresh + lazy builder
return RefreshIndicator(
onRefresh: () async {
_posts.clear();
_hasMore = true;
await _loadMore();
},
child: ListView.separated(
controller: _scroll,
itemCount: _posts.length + (_hasMore ? 1 : 0),
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, i) {
if (i >= _posts.length) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
final p = _posts[i];
return ListTile(
leading: CircleAvatar(backgroundImage: NetworkImage(p.avatarUrl)),
title: Text(p.title, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(p.summary, maxLines: 2, overflow: TextOverflow.ellipsis),
trailing: Text(p.timeAgo),
onTap: () => Navigator.pushNamed(context, '/post', arguments: p.id),
);
},
),
);
}
@override
void dispose() {
_scroll.dispose();
super.dispose();
}
}
// 2) Static list — fine for a fixed small list
ListView(
children: const [
ListTile(title: Text('One')),
ListTile(title: Text('Two')),
ListTile(title: Text('Three')),
],
);
// 3) Horizontal list
SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: items.length,
itemBuilder: (_, i) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: CategoryCard(item: items[i]),
),
),
);
// 4) SliverList in a CustomScrollView — when you mix lists with other content
CustomScrollView(
slivers: [
const SliverAppBar(title: Text('Feed'), pinned: true),
SliverList.builder(
itemCount: posts.length,
itemBuilder: (_, i) => ListTile(title: Text(posts[i].title)),
),
],
);
// 5) Performance tips
// • Use ListView.builder for >20 items — never the eager constructor
// • Give items a Key when reordering — Flutter can preserve state
// • If items have fixed height, set itemExtent for big lists
// • Avoid building heavy widgets inside itemBuilder — use const where possible
Why it matters
For real feeds, always: ListView.builder (lazy), RefreshIndicator (pull-to-refresh), a scroll listener for infinite scroll, and SliverAppBar when you want collapsing headers above the list.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
ListView.builder(
itemCount: users.length,
itemBuilder: (_, i) => ListTile(
title: Text(users[i].name),
subtitle: Text(users[i].email),
),
)
Try it Yourself »
Exercise
Build list rows lazily.
ListView.
(itemCount: 100, itemBuilder: (_, i) => Text('row $i'))
Seven letters.
Discussion
Loading…