Intermediate · Lists, navigation and forms · topic 22 of 50 · 30 min
Lists with ListView
Explanation
Why ListView.builder?
Most apps are lists: chats, products, transactions, notifications. A list may have thousands of items. Building every item at once wastes memory and time, especially on budget phones. ListView.builder builds only the items currently visible (plus a few around them) and builds new ones as the user scrolls. It is called lazy building.
class Txn {
const Txn(this.title, this.amount);
final String title;
final int amount;
}
class StatementList extends StatelessWidget {
const StatementList({super.key, required this.txns});
final List<Txn> txns;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: txns.length,
itemBuilder: (context, index) {
final t = txns[index];
return ListTile(
leading: Icon(t.amount < 0 ? Icons.arrow_upward : Icons.arrow_downward),
title: Text(t.title),
trailing: Text('Rs ${t.amount.abs()}'),
);
},
);
}
}itemCount tells the builder how many items exist, so the list knows where it ends and how long the scrollbar should be. Leave it out only for truly endless lists, and then return null from itemBuilder when there are no more items. Each call to itemBuilder should be cheap: read the item, build a small widget, nothing more.
Lists usually get their data from state (a List in a StatefulWidget, a Provider or a Bloc). When that list changes and the widget rebuilds, ListView.builder shows the new items; you never add or remove rows by hand.
The three ListView constructors
| Constructor | Builds items | Use for |
|---|---|---|
| ListView(children: [...]) | All at once | Short, fixed lists, such as a settings page |
| ListView.builder | Lazily, on demand | Long or dynamic lists |
| ListView.separated | Lazily, with a separator between items | Lists with dividers |
ListView.separated(
itemCount: contacts.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) => ListTile(title: Text(contacts[index])),
)ListTile
ListTile is a Material row layout with slots: leading (an icon or avatar), title, subtitle, trailing (an icon, a switch or text) and onTap. It handles padding, height and accessibility for you, so most list rows can start as a ListTile.
Swipe and pull
RefreshIndicator(
onRefresh: () async => await reloadOrders(), // must return a Future
child: ListView.builder(
itemCount: orders.length,
itemBuilder: (context, i) => Dismissible(
key: ValueKey(orders[i].id), // a unique, stable key per item
onDismissed: (_) => removeOrder(orders[i].id),
background: Container(color: Colors.red),
child: ListTile(title: Text(orders[i].title)),
),
),
)A key is a stable identity for a widget. When items move or are removed, keys let Flutter match old and new items correctly, so a swipe or animation affects the right row. Use something unique from your data, such as an id, with ValueKey.
Common mistakes
- Using ListView(children: list.map(...).toList()) for hundreds of items: slow. Use ListView.builder.
- Putting a ListView inside a Column without Expanded: unbounded height error (module 5).
- Using the index as the Dismissible key: after a delete, indexes shift and the wrong item is affected. Use a unique id.
- Doing heavy work (date formatting, parsing) inside itemBuilder for every item: prepare data beforehand.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Lists with ListView · 1/6
Most apps are lists. ListView shows a scrollable list of widgets.
Prefer a video by a teacher? Find free videos on “flutter listview builder separated listtile dismissible refreshindicator” on YouTube ↗
In real life: A notification list
A banking app shows hundreds of notifications. ListView.separated builds only the visible ones, each a ListTile with an icon, title and time. Users pull down to refresh and swipe to delete a notification. Because each Dismissible uses the notification's id as its key, deleting one never removes the wrong row.
ListView.separated(
itemCount: notes.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, i) {
final n = notes[i];
return ListTile(
leading: const Icon(Icons.notifications),
title: Text(n.title),
subtitle: Text(n.time),
onTap: () => openNote(n.id),
);
},
)Practice (0/3 done)
Which is best for a chat list with 5,000 messages?
Why should a Dismissible use the item's id rather than its index as the key?
Build a shopping list screen: a ListView.separated of items (name and quantity) using ListTile, swipe-to-delete with Dismissible and a SnackBar "Removed X", pull-to-refresh that resets the list after a short delay, and an empty-state message when the list is empty.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Grids with GridViewLists, navigation and forms · same module
- Navigator basicsLists, navigation and forms · same module
- Routing with go_routerLists, navigation and forms · same module
- Lists and gridsKotlin and Android App Development · Jetpack Compose UI
- Interview questions: FlutterFlutter careers and interviews
- PerformanceAnimations and performance
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does lazy building mean for ListView.builder?
Answer: Items are built only when they are about to be visible
Saves memory and time.
Which ListTile slot usually holds an icon or avatar at the start?
Answer: leading
leading is at the start.
What must RefreshIndicator's onRefresh return?
Answer: A Future
The spinner shows until the Future completes.
When is the plain ListView(children: [...]) a good choice?
Answer: For short, fixed lists
All children are built at once.
What is the problem with shrinkWrap: true on a long ListView?Related
Answer: It builds all items at once, hurting performance
Prefer Expanded or a single scrollable.
Which is the right way to build a scrolling screen with a header and a long list?Related
Answer: One LazyColumn with item { header } and items { }
Nested same-direction scrolling crashes.
From Lists and grids
Which widget adds a tap ripple to a tile?Related
Which object holds a StatefulWidget's State between rebuilds?Related
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 4 more topics in “Lists, navigation and forms” before its exam: Grids with GridView, Navigator basics, Routing with go_router, Forms and validation.
- Module 6 exam: Lists, navigation and forms · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, State management. 50 topics left in the course before the final exam.