Dart Tutorial

Flutter Lesson 19 of 83 3 min read

ListView in Flutter: Scrolling Lists

Learn ListView in Flutter: ListView.builder for long lists, separators, ListTile, horizontal lists, and fixing common list errors.

On this page

ListView is a scrolling column. It is the widget behind message lists, feeds, settings pages and most data screens.

A short, fixed list #

ListView(
  padding: const EdgeInsets.all(16),
  children: const [
    ListTile(leading: Icon(Icons.wifi), title: Text('Wi-Fi')),
    ListTile(leading: Icon(Icons.bluetooth), title: Text('Bluetooth')),
    ListTile(leading: Icon(Icons.battery_full), title: Text('Battery')),
  ],
)

This builds every child immediately. That is fine for a dozen items.

ListView.builder for long lists #

ListView.builder builds items only when they scroll into view. It handles ten items or a million with the same memory.

import 'package:flutter/material.dart';

void main() {
  final contacts = List.generate(500, (i) => 'Contact ${i + 1}');

  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Contacts')),
        body: ListView.builder(
          itemCount: contacts.length,
          itemBuilder: (context, index) {
            final name = contacts[index];
            return ListTile(
              leading: CircleAvatar(child: Text('${index + 1}')),
              title: Text(name),
              subtitle: const Text('Tap to view'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () => debugPrint('Tapped $name'),
            );
          },
        ),
      ),
    ),
  );
}

itemBuilder is called with an index and returns the widget for that row. Use the builder whenever the list comes from data.

Separators #

ListView.separated(
  itemCount: items.length,
  separatorBuilder: (context, index) => const Divider(height: 1),
  itemBuilder: (context, index) => ListTile(title: Text(items[index])),
)

ListTile #

ListTile is a ready-made row with slots.

SlotUsually holds
leadingAn icon or avatar
titleThe main text
subtitleSecondary text
trailingAn icon, switch or value
onTapWhat happens on tap

For a row that does not fit this shape, return your own widget from itemBuilder.

Horizontal lists #

SizedBox(
  height: 120,
  child: ListView.separated(
    scrollDirection: Axis.horizontal,
    padding: const EdgeInsets.symmetric(horizontal: 16),
    itemCount: 10,
    separatorBuilder: (context, index) => const SizedBox(width: 12),
    itemBuilder: (context, index) => Container(
      width: 160,
      decoration: BoxDecoration(
        color: Colors.teal.shade100,
        borderRadius: BorderRadius.circular(12),
      ),
      alignment: Alignment.center,
      child: Text('Card $index'),
    ),
  ),
)

A horizontal list needs a fixed height from its parent.

A list inside a Column #

Placing a ListView directly in a Column throws “unbounded height”. Give it the remaining space with Expanded.

Column(
  children: [
    const Padding(padding: EdgeInsets.all(16), child: Text('Recent orders')),
    Expanded(
      child: ListView.builder(
        itemCount: orders.length,
        itemBuilder: (context, i) => ListTile(title: Text(orders[i])),
      ),
    ),
  ],
)

You may see shrinkWrap: true suggested. It works, but it makes the list build all its items to measure itself, which defeats the purpose of the builder. Use it only for a handful of items. For a page that mixes headers and lists and scrolls as one, use slivers.

Empty lists #

body: items.isEmpty
    ? const Center(child: Text('Nothing here yet'))
    : ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, i) => ListTile(title: Text(items[i])),
      ),

Controlling the scroll #

final _controller = ScrollController();

// Scroll to the top with an animation.
_controller.animateTo(
  0,
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeOut,
);

// Remember to call _controller.dispose() in dispose().

Pass it to the list with controller: _controller.

Performance #

  • Use ListView.builder or ListView.separated for anything from data.
  • When every row has the same height, set itemExtent. Scrolling to a position becomes instant.
  • Keep row widgets const where you can, and keep their build methods light.

Try it yourself #

Build a to-do list screen from a List<String>. Show each task in a ListTile with a checkbox on the left, dividers between rows, and a friendly message when the list is empty.

Practise in the playground Updated by Santosh Adhikari