Skip to content

ListView

A scrollable, keyboard-navigable list of items, vertical by default or horizontal with scrollDirection.

See also: ScrollView for one tall non-list child · DataTable for rows with named columns.

Learn more: Lists & scrolling

Import: package:fleury/fleury.dart, or package:fleury/fleury_core.dart in browser code.

ListView.builder(
itemCount: rows.length,
itemBuilder: (context, i, highlighted) => Text(rows[i].label),
)

Two ways to populate the list:

  • ListView(children: [...]) — eager. Every child widget is built upfront on each rebuild; the layout/paint pass only visits items that fit in the viewport. Best when you have a bounded set of widgets you already constructed.
  • ListView.builder(itemCount: N, itemBuilder: (context, index, highlighted) {}) — lazy. Only items currently within the viewport are mounted as element subtrees; items scroll into/out of the mounted set as the user navigates. Supports variable sizes along the scrolling axis. Best for long lists where most items are off-screen (file pickers, log viewers, completion menus).

When focused, the list handles its main-axis arrows (Up and Down, or Left and Right in a horizontal list), PageUp, PageDown, Home, End, and Enter:

  • The arrows move the current item by one, PageUp and PageDown by the number of items in view, and Home and End to the first and last item. A move reports onFocusedItemChanged and scrolls the item into view.
  • Enter or a completed click selects the current item via onSelect.
  • An arrow or page key pressed on the first item toward the start, or on the last toward the end, follows edgeBehavior: contain consumes the key, bubble returns it to the focus chain so an ancestor KeyBindings (e.g. one coordinating sidebar + main pane focus traversal) can react. From any other item, a page key stops at the first or last item.
  • A list without a current item (selectable: false, or a controller with no cursor) scrolls instead: the arrows by one cell and PageUp and PageDown by a viewport, following edgeBehavior once scrolled to an end, while Home and End jump to the start and end.

itemBuilder receives (context, index, highlighted) for each visible item. The current item remains highlighted when keyboard focus leaves the list. The controller’s ListController.currentIndex identifies that item. Setting it moves the cursor without selecting an item or taking focus.

All constructors apply the theme’s selection style to the current row’s text. Plain Text children need no cursor styling. Explicit child styles override that default; the builder flag is available for custom decoration.

Eager constructor: build all items upfront from a fixed list of widgets. Use when you have a bounded set of widgets already constructed. The current row is highlighted automatically.

ParameterTypeDefaultDescription
controller:ListController?—External controller. If null, the widget creates its own and disposes it on unmount.
focusNode:FocusNode?—External FocusNode. Provide one when a parent needs to drive focus (e.g. Tab cycling between sidebar and main pane). If null, the widget creates its own and disposes it on unmount.
children:List<Widget>requiredPre-built widgets (eager form).
autofocus:boolfalseWhether to request focus on first mount.
selectable:booltrueWhether the list owns a row cursor and selects rows with click or Enter. False keeps it scrollable without a row cursor. Interactive child widgets keep their own input behavior.
edgeBehavior:EdgeBehaviorEdgeBehavior.bubbleHow main-axis arrows and wheel gestures behave at an edge. Cross-axis input can reach other controls or a surrounding scroll view.
onSelect:void Function(int index)?—Selects an item on Enter or a completed click, including repeated choices. Browsing, scrolling, and controller writes do not call this. Empty lists and lists without a cursor cannot select an item.
onFocusedItemChanged:void Function(int index)?—Reports user input moving the cursor to a different item. More
scrollbar:boolfalseWhen true, wrap the list in a Scrollbar gutter that reflects the visible item range and lets the mouse drag/click to scroll. A one-line opt-in: the bar shares this list’s controller, so there is nothing extra to wire. See Scrollbar.list. More
scrollDirection:AxisAxis.verticalLayout and scrolling axis. Items are measured at their natural extent along this axis and constrained to the viewport on the other axis.
addRepaintBoundaries:booltrueWhether each item gets its own RepaintBoundary, so an update inside one item (a row’s setState, a line of streaming output) repaints only that item rather than the whole list. Items stay interactive and accessible either way. More
  • onFocusedItemChanged: Programmatic controller writes and identity-preserving data updates do not call this callback.
  • scrollbar: The gutter needs a bounded cross axis: width for a vertical list, height for a horizontal list. The list itself needs a bounded main axis.
  • addRepaintBoundaries: Turn it off only for lists of trivially cheap items, where the per-item boundaries would cost more than the painting they save.

Lazy constructor: build items on demand by index, mount only the visible ones. Each item builder invocation receives a highlighted flag for styling the current row.

ParameterTypeDefaultDescription
controller:ListController?—External controller. If null, the widget creates its own and disposes it on unmount.
focusNode:FocusNode?—External FocusNode. Provide one when a parent needs to drive focus (e.g. Tab cycling between sidebar and main pane). If null, the widget creates its own and disposes it on unmount.
itemCount:intrequiredNumber of items (lazy form).
itemBuilder:Widget Function(BuildContext, int, bool)requiredPer-index widget builder (lazy form). Invoked with (context, index, highlighted).
itemKeyBuilder:ListItemKeyBuilder?—Returns a stable key for the data item at each index. Supply it when items can move (prepends, removals, filters, reorders) so the current item, the scroll position, and the state of mounted items follow each item rather than its index. Keys must be unique within the list and have stable equality and hash codes; a duplicate key throws a StateError. More
autofocus:boolfalseWhether to request focus on first mount.
selectable:booltrueWhether the list owns a row cursor and selects rows with click or Enter. False keeps it scrollable without a row cursor. Interactive child widgets keep their own input behavior.
edgeBehavior:EdgeBehaviorEdgeBehavior.bubbleHow main-axis arrows and wheel gestures behave at an edge. Cross-axis input can reach other controls or a surrounding scroll view.
onSelect:void Function(int index)?—Selects an item on Enter or a completed click, including repeated choices. Browsing, scrolling, and controller writes do not call this. Empty lists and lists without a cursor cannot select an item.
onFocusedItemChanged:void Function(int index)?—Reports user input moving the cursor to a different item. More
scrollbar:boolfalseWhen true, wrap the list in a Scrollbar gutter that reflects the visible item range and lets the mouse drag/click to scroll. A one-line opt-in: the bar shares this list’s controller, so there is nothing extra to wire. See Scrollbar.list. More
scrollDirection:AxisAxis.verticalLayout and scrolling axis. Items are measured at their natural extent along this axis and constrained to the viewport on the other axis.
addRepaintBoundaries:booltrueWhether each item gets its own RepaintBoundary, so an update inside one item (a row’s setState, a line of streaming output) repaints only that item rather than the whole list. Items stay interactive and accessible either way. More
  • itemKeyBuilder: This is data identity only: it does not install a Fleury Key on the row or create a semantic identifier. Add those at the item-widget layer when the application needs either contract.

    Fleury reads all item keys once on mount and whenever the parent supplies an updated ListView. Checking keys takes O(itemCount) time; unchanged ordered keys reuse the existing reverse lookup. Changed keys rebuild it in O(itemCount) time and space. Row widgets are still built and laid out only as needed.

Lazy constructor with separators — the TUI analogue of Flutter’s ListView.separated. separatorBuilder is called for each gap i — the space between item i and item i + 1, so 0 <= i <= itemCount - 2 — and may return null to omit that gap’s separator (e.g. a day divider shown only when the day actually changes).

Separators never take the cursor and hold no index of their own — the list still addresses exactly itemCount items, and arrow / Home / End navigation walks items only. Each follows its item along scrollDirection, and only the item is a tap target. A separator cannot move the cursor or select the item it trails.

ParameterTypeDefaultDescription
controller:ListController?—External controller. If null, the widget creates its own and disposes it on unmount.
focusNode:FocusNode?—External FocusNode. Provide one when a parent needs to drive focus (e.g. Tab cycling between sidebar and main pane). If null, the widget creates its own and disposes it on unmount.
itemCount:intrequiredNumber of items (lazy form).
itemBuilder:Widget Function(BuildContext, int, bool)requiredPer-index widget builder (lazy form). Invoked with (context, index, highlighted).
separatorBuilder:Widget? Function(BuildContext, int)requiredPer-gap separator builder (ListView.separated form). Called with (context, i) for the gap between item i and item i + 1; may return null to omit that separator.
itemKeyBuilder:ListItemKeyBuilder?—Returns a stable key for the data item at each index. Supply it when items can move (prepends, removals, filters, reorders) so the current item, the scroll position, and the state of mounted items follow each item rather than its index. Keys must be unique within the list and have stable equality and hash codes; a duplicate key throws a StateError. More
autofocus:boolfalseWhether to request focus on first mount.
selectable:booltrueWhether the list owns a row cursor and selects rows with click or Enter. False keeps it scrollable without a row cursor. Interactive child widgets keep their own input behavior.
edgeBehavior:EdgeBehaviorEdgeBehavior.bubbleHow main-axis arrows and wheel gestures behave at an edge. Cross-axis input can reach other controls or a surrounding scroll view.
onSelect:void Function(int index)?—Selects an item on Enter or a completed click, including repeated choices. Browsing, scrolling, and controller writes do not call this. Empty lists and lists without a cursor cannot select an item.
onFocusedItemChanged:void Function(int index)?—Reports user input moving the cursor to a different item. More
scrollbar:boolfalseWhen true, wrap the list in a Scrollbar gutter that reflects the visible item range and lets the mouse drag/click to scroll. A one-line opt-in: the bar shares this list’s controller, so there is nothing extra to wire. See Scrollbar.list. More
scrollDirection:AxisAxis.verticalLayout and scrolling axis. Items are measured at their natural extent along this axis and constrained to the viewport on the other axis.
addRepaintBoundaries:booltrueWhether each item gets its own RepaintBoundary, so an update inside one item (a row’s setState, a line of streaming output) repaints only that item rather than the whole list. Items stay interactive and accessible either way. More

ListView is defined in packages/fleury/lib/src/widgets/list_view.dart.

Category: Lists & data · All widgets