Kodokon kodokon.com

High-performance lists with ListView.builder

Display lists of thousands of items without slowdowns thanks to ListView.builder, ListTile and items extracted into their own widgets.

8 min · 3 questions

Open this lesson in Kodokon

A 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.

DART
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]),
          );
        },
      ),
    );
  }
}
Only the visible items are built, even with 10,000 names.

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.

DART
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,
    );
  }
}
A list item turned into a reusable, testable widget.

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.

DART
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]),
        );
      },
    );
  }
}
separatorBuilder inserts a Divider between each pair of items.

Knowledge check

Make sure you remember the key points of this lesson.

  1. Why prefer ListView.builder over ListView(children: ...) for a list of 500 items?
    • It automatically sorts the items
    • It only builds the visible items, on demand
    • It caches images
    • It automatically adds separators
  2. Which constructor inserts a widget between each pair of items?
    • ListView.divided
    • ListView.builder
    • ListView.separated
  3. What is the benefit of extracting a list item into its own widget class?
    • It is required for ListView.builder to compile
    • It enables const, isolates rebuilds and makes reuse easier
    • It makes the list scrollable
    • It saves you from writing itemCount