The first time you need a scrolling list in a Flutter app, you reach for Column, stack a bunch of widgets inside it, run the app, and watch the bottom half of your screen get clipped by an overflow stripe. That stripe is Flutter politely telling you that Column will not scroll, and that for anything longer than the screen you wanted a Flutter ListView all along. ListView is the widget that handles scrolling lists out of the box, and once you have the right mental model it stops feeling like one of a hundred random widgets and starts feeling like the obvious answer. This post walks you through what a Flutter ListView is, the two main ways to build one, and the small details that catch every beginner.
What a Flutter ListView actually does
A Flutter ListView is a vertical, scrollable column of children. You hand it a list of widgets, and it lays them out top to bottom and lets the user swipe through them. The scrolling is free. The clipping is free. The bouncy edge on iOS is free. You do not write any of that yourself, which is the entire point.
Under the hood, ListView is a kind of widget called a sliver, which is Flutter’s term for something that knows how to participate in a scrolling area. You do not need to understand slivers to use ListView, but it helps to know that the word will pop up later when you want fancy effects like collapsing app bars. For now, treat ListView as “Column, but it scrolls” and you have most of the mental picture right.
There is one trap worth knowing about up front. ListView does not play well inside a Column unless you tell it how much space to take. If you drop a ListView straight into a Column, Flutter complains because ListView wants infinite vertical space and Column wants to lay out finite children. The fix is to wrap the ListView in an Expanded widget, which tells the parent Column to give the ListView whatever room is left. You will hit this within your first hour of using ListView, so save yourself a search.
The other thing to know is that ListView is happiest when it is the body of a Scaffold, or close to it. The body of a Scaffold has known height, ListView fills it, scrolling works, you move on with your life. Most beginner Flutter list problems trace back to that single rule. Once your ListView is the body of a Scaffold (or wrapped in Expanded inside one), the widget cooperates and the overflow stripes stop showing up.
ListView and ListView.builder, side by side
For short lists, the plain ListView constructor is the simplest thing that works. You hand it a children list, and it shows them in order.
import 'package:flutter/material.dart';
class FavoriteFoods extends StatelessWidget {
const FavoriteFoods({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Favorite foods')),
body: ListView(
children: const [
ListTile(leading: Icon(Icons.local_pizza), title: Text('Pizza')),
ListTile(leading: Icon(Icons.cake), title: Text('Birthday cake')),
ListTile(leading: Icon(Icons.local_cafe), title: Text('Coffee')),
ListTile(leading: Icon(Icons.icecream), title: Text('Ice cream')),
],
),
);
}
}
Run that and you will get four tappable rows with icons on the left, in a column you can scroll. ListTile is a convenience widget for the row pattern. You could use any widget you want there, including custom ones you have built yourself.
The catch is that plain ListView builds every child up front, whether the user ever scrolls to them or not. For four foods that is fine. For four hundred messages, it gets slow. That is when you switch to ListView.builder, which only builds the children currently on screen plus a small buffer. The API looks like this.
ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final m = messages[index];
return ListTile(
leading: const Icon(Icons.message_outlined),
title: Text(m.subject),
subtitle: Text(m.preview),
);
},
)
Two parameters, both required. itemCount tells Flutter how many items the list has, so it can size the scroll bar and know when to stop. itemBuilder is a function Flutter calls once per visible row, passing the index. Inside itemBuilder you return the widget for that row. Flutter recycles the rest, the way a native iOS table view or an Android RecyclerView does, and your scrolling stays smooth even on long lists.
Pick the plain constructor for short, fixed lists. Pick ListView.builder for anything that comes from a database, a network call, or any list longer than a screen or two. When in doubt, reach for ListView.builder. The code looks almost identical and you get the performance for free.

Adding dividers with ListView.separated
The third constructor you will use a lot is ListView.separated. It is ListView.builder with one extra parameter, a separatorBuilder, which Flutter calls between each pair of items. This is the cleanest way to drop a divider line, some spacing, or a styled section break between rows.
ListView.separated(
itemCount: notes.length,
itemBuilder: (context, index) {
final note = notes[index];
return ListTile(
title: Text(note.title),
subtitle: Text(note.body),
);
},
separatorBuilder: (context, index) => const Divider(height: 1),
)
Run that and you get a tidy list of notes with a thin horizontal line between each pair. The Divider widget is purpose-built for this, with sensible defaults for color and spacing that match the platform. Swap it for SizedBox(height: 12) if you just want a gap, or wrap a Container in there for a stripe of color, or build any custom separator you want.
One small detail trips people up. The separatorBuilder is called itemCount minus one times, not itemCount times. So if you have ten items, there are nine separators, between them. You do not need to do the math yourself, but knowing this answers the “why is there no separator after my last item” question that comes up at some point. If you want a trailing line, add it as the last item of the list, not as a separator.
ListView.separated also takes most of the same optional parameters as ListView.builder, including padding, scrollDirection, and reverse. The reverse parameter is especially handy for chat-style lists, where the newest message sits at the bottom of the screen and the older ones scroll up. Set reverse to true and Flutter starts the list from the bottom, which matches the conventions of most messaging apps without any extra code.

Common mistakes and where to go from here
Three Flutter ListView mistakes show up almost every time a beginner builds their first scrolling list. The first is putting a ListView inside a Column without an Expanded wrapper, and getting an “infinite size” error. The fix is the one you already saw: wrap the ListView in Expanded, or restructure so the ListView is the direct body of the Scaffold.
The second mistake is using ListView (the plain constructor) for a list of hundreds of items pulled from a database, and wondering why the app feels sluggish. Switch to ListView.builder and the problem goes away. There is no penalty for using the builder on a list of three items, so it is fine to make builder your default.
The third one is calling setState inside the itemBuilder function. The builder is meant to be pure, in the sense that it returns the same widget given the same inputs. If you trigger a rebuild from inside it, you can spiral into rebuild loops that hurt performance and confuse the debugger. Keep your state changes in callbacks, like onTap, and let itemBuilder just describe the row.
Once you have plain ListView, ListView.builder, and ListView.separated in your toolkit, you can build almost any list a beginner project needs. If you want to revisit the widget basics, our post on Flutter widgets for beginners pairs nicely with this one, and the Flutter layouts walkthrough explains where ListView fits next to Column and Row. If your list is still showing overflow stripes after this post, our common Flutter mistakes roundup covers the usual culprits. For the deepest reference, the official Flutter ListView documentation lays out every parameter in detail.


Pingback: Dart List Methods Every Flutter Beginner Should Know - Coding Guru