Display lists of thousands of items without slowdowns thanks to ListView.builder, ListTile and items extracted into their own widgets.
Open this lesson in KodokonA real-world app almost always displays lists: messages, products, contacts. The ListView(children: ...) constructor you already know builds all of its children at once - fine for ten items, disastrous for a thousand. ListView.builder only builds the items near the visible area, on demand, as you scroll.
import 'package:flutter/material.dart';
class ContactsScreen extends StatelessWidget {
const ContactsScreen({super.key, required this.names});
final List<String> names;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Contacts')),
body: ListView.builder(
itemCount: names.length,
itemBuilder: (context, index) {
return ListTile(
leading: const Icon(Icons.person),
title: Text(names[index]),
);
},
),
);
}
}Two parameters carry the whole mechanism: itemCount declares the total number of items, and itemBuilder produces the item at a given index, only when it approaches the screen. For the content of each row, ListTile is your Material ally: leading, title, subtitle and trailing structure the row, and onTap provides the ink ripple on touch. But as soon as an item grows, don't let it balloon inside itemBuilder: extract it into its own widget.
import 'package:flutter/material.dart';
class ContactTile extends StatelessWidget {
const ContactTile({
super.key,
required this.name,
required this.onTap,
});
final String name;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return ListTile(
leading: CircleAvatar(child: Text(name[0])),
title: Text(name),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
}An extracted item can be tested in isolation, reused across other screens, and makes the itemBuilder trivial. One frequent need remains: separators. ListView.separated works like builder with a third parameter, separatorBuilder, called between each pair of items - the last element is never followed by a separator.
import 'package:flutter/material.dart';
class ContactList extends StatelessWidget {
const ContactList({super.key, required this.names});
final List<String> names;
@override
Widget build(BuildContext context) {
return ListView.separated(
itemCount: names.length,
separatorBuilder: (context, index) {
return const Divider(height: 1);
},
itemBuilder: (context, index) {
return ContactTile(
name: names[index],
onTap: () => debugPrint(names[index]),
);
},
);
}
}