Theory
The event list, third and final time
This subject OPENED with the fest's event list: Android's ListView, an adapter, getView, recycled thalis. Twenty-two lessons later the same 40 events need listing in Flutter.
This is deliberate: watching ONE problem solved by 2 frameworks is where real understanding lives. Flutter's answer has 2 forms: an eager one for handfuls: and a lazy one whose heart, when you meet it, will make you smile in recognition.
Theory
Form 1: the eager ListView
ListView(children: [widgetA, widgetB, widgetC])
Hand it the children; it scrolls them. Every child is built immediately, whether visible or not: perfectly fine for a SETTINGS page of 8 rows, wasteful for anything that grows.
Because a ListView scrolls itself, Unit 1's law crosses over intact: never wrap one in another same-axis scroller: gesture wars know no framework.
Practical
Form 2: the lazy builder (read itemBuilder twice)
final events = List.generate(40, (i) => 'Event ${i + 1}');
ListView.builder(
itemCount: events.length, // how many rows exist
itemBuilder: (context, index) { // called ONLY for visible rows
return ListTile(
leading: const Icon(Icons.event), // the left slot
title: Text(events[index]), // main line
subtitle: const Text('Main Ground'), // secondary line
trailing: const Icon(Icons.chevron_right),
onTap: () {
// open this event's booking screen
debugPrint('Tapped ${events[index]}');
},
);
},
)
Theory
itemBuilder is getView, reborn
Line the 2 frameworks up:
- Android's adapter answered getView(position, ...): build/refill the row for this position
- Flutter's itemBuilder(context, index): build the row for this index
Same contract: a function that manufactures ONE row on demand. And the recycling? Flutter's builder is lazy by design: rows are built only as they scroll into view and disposed as they leave: the convertView dance you hand-coded in Unit 1, now performed by the framework with no null-check for you to forget. 40 events or 40000: only the visible dozen exist at any moment.
Theory
ListTile: the row you stop hand-building
Unit 1's custom row needed a layout XML and findViewById choreography. Flutter ships the standard row as a widget, ListTile, with named slots:
- leading: the left slot: icon or avatar
- title / subtitle: main and secondary lines
- trailing: the right slot: a chevron, a price
- onTap: the built-in row tap
Richer rows than ListTile allows? Return any widget tree from itemBuilder: a Card holding your own Column: composition, as ever. And dividers between rows: ListView.separated, which adds a separatorBuilder alongside.
Quiz
For a 5000-row list, why is ListView.builder dramatically better than ListView(children: [...])?
- builder rows are cached to disk automatically
- itemBuilder is called lazily, only for rows scrolling into view: a dozen live rows instead of 5000 built up front
- builder lists scroll faster because they skip animations
- There is no real difference: both build all rows
Show the answer
itemBuilder is called lazily, only for rows scrolling into view: a dozen live rows instead of 5000 built up front
The children form constructs all 5000 widgets before the first frame: memory and jank: while builder manufactures rows on demand and disposes them off-screen: only the visible handful exist. This is Unit 1's recycling insight (few thalis, endless queue) made automatic: Android asked you to honour convertView; Flutter's builder simply never builds what is not needed. Options A and C invent mechanisms: no disk cache, no animation skipping: laziness alone is the win. The exam phrasing to keep: builder = lazy, on-demand construction of visible items.
Think first
Translate your Unit 1 vocabulary
Complete the dictionary from Android's ListView to Flutter's, entry by entry: adapter, getView(position), convertView recycling, setOnItemClickListener, notifyDataSetChanged. Then tap.
Show the answer
Adapter: the builder constructor itself (data + row factory in one place). getView(position): itemBuilder(context, index). convertView recycling: automatic laziness: no null-check, the framework builds/disposes. setOnItemClickListener(position): onTap on the row you build: the index is already in scope, no listener juggling. notifyDataSetChanged: setState around the data change: the one Flutter answer to every update. Five entries, one lesson: frameworks change the spelling; the LIST stays the same problem.
Watch out
List slips
children form for big data: compiles, runs, janks: reach for builder the moment the list is data-driven.
Forgetting itemCount: an infinite builder happily asks for index 40 of a 40-item list: RangeError: itemCount is the fence.
A ListView inside a Column without bounds: unbounded-height errors: give it an Expanded wrapper (the stripes lesson's cousin).
Mutating the list without setState: rows will not appear or vanish: the controlled law governs data too.
Theory
The circle closes
First lesson of this subject: an Android list, an adapter, recycled rows. Third-last lesson: the same list, and the adapter idea so absorbed that Flutter's version reads as an old friend. That arc IS the subject: patterns over platforms. Two lessons remain: dialogs and tooltips (the polite interruptions), then toast, switch, charts and the Form that finishes FestConnect Flutter for good.
Summary
Key takeaways
- ListView(children:) builds every row up front: for small fixed lists only.
- ListView.builder(itemCount, itemBuilder): lazy: rows built only as they scroll into view, disposed after.
- itemBuilder(context, index) is Unit 1's getView(position) with recycling made automatic.
- ListTile: leading / title / subtitle / trailing / onTap: the ready-made Material row; richer rows are any widget tree.
- ListView.separated adds dividers via separatorBuilder.
- Lists scroll themselves (no nesting in same-axis scrollers); data changes go through setState.
- Memory hook: builder builds only what the eye can see.