ListView and scrolling

Flutter · Essentials · 12 min

Feeds, chats, settings and search results are all lists, and building them lazily keeps apps fast.

Content taller than the screen must scroll.

  • `SingleChildScrollView` makes one child (often a Column) scrollable. Fine for forms and short pages; it builds everything at once.
  • `ListView(children: [...])` — a scrolling list of a fixed set of widgets, also built eagerly.
  • `ListView.builder(itemCount: n, itemBuilder: (context, i) => ...)` builds items lazily, only those near the viewport. Use it for long or unbounded lists; thousands of items cost no more than a screenful.
  • `ListView.separated` adds a separator widget between items.
  • `GridView.builder` with a SliverGridDelegateWithFixedCrossAxisCount or ...MaxCrossAxisExtent for grids.

ListTile is a ready-made row with leading, title, subtitle, trailing and onTap.

Scroll behavior: control position with a ScrollController (create in initState, dispose in dispose), listen for the end of the list to load more, and use RefreshIndicator for pull-to-refresh.

A classic error: putting a ListView directly inside a Column. The list wants infinite height and the Column offers unbounded height, causing "Vertical viewport was given unbounded height". Wrap the list in Expanded so it gets the remaining space. Avoid shrinkWrap: true on long lists — it defeats lazy building.

Give list items stable keys when items can be reordered or removed.

Example

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 Column(
      children: [
        const Padding(padding: EdgeInsets.all(16), child: Text('Contacts')),
        Expanded(                                  // give the list bounded height
          child: ListView.separated(
            itemCount: names.length,               // built lazily
            separatorBuilder: (context, i) => const Divider(height: 1),
            itemBuilder: (context, i) => ListTile(
              key: ValueKey(names[i]),
              leading: CircleAvatar(child: Text(names[i][0])),
              title: Text(names[i]),
              onTap: () => debugPrint('tapped ${names[i]}'),
            ),
          ),
        ),
      ],
    );
  }
}

Remember

  • ListView.builder/separated build items lazily — use them for long lists
  • A ListView inside a Column needs Expanded
  • ListTile is the standard row; GridView.builder for grids

Watch out for

  • Using shrinkWrap: true on a long list inside another scroll view, which builds every item.

Check yourself

  1. Why use ListView.builder for a list of 10,000 messages?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/cookbook/lists/long-lists