Lists¶
FlatList and
SectionList are the two list components
shipped with PythonNative. Both are virtualized: only the rows
inside (and just beyond) the viewport are mounted as native views.
On mobile, RecyclerView (Android) and UICollectionView (iOS) own physical
cell recycling. Rows remain ordinary keyed components in the application's
logical tree. Providers, error boundaries, suspense boundaries, and cancellation
therefore work across row and screen containers.
Use stable keys and immutable data. A new data snapshot advances the list's
revision; stale row requests are discarded. estimated_item_height provides an
initial estimate for variable content, and native measurement refines row
extents. Fixed heights, sections, grids, headers, footers, and pull-to-refresh
use this native path too. The browser implements the same logical-row protocol;
headless tests use a windowed scroll container.
import pythonnative as pn
items = [{"id": i, "title": f"Row {i}"} for i in range(10_000)]
@pn.component
def Big():
return pn.FlatList(
data=items,
item_height=44,
render_item=lambda item, _: pn.Text(item["title"]),
key_extractor=lambda item, _: str(item["id"]),
)
The list never holds 10,000 native views; only the window around the viewport ever exists.
Data snapshots¶
Lists accept a Sequence and cache keys and row metadata separately from the
scrolling window. Keep render_item, key_extractor, and height callbacks
stable when the parent rerenders, using module-level functions or
use_callback. Scrolling then does work proportional to the mounted window.
Prefer replacing data with a new sequence. If you mutate a sequence in place,
increment data_revision when passing it to FlatList or SectionList.
The revision invalidates the cached snapshot. Native prefetch requests mount
nearby rows, and keyed anchoring preserves the visible row as measurements or
data updates change extents above it.
Row heights¶
Three ways to tell the list how tall rows are, in order of preference:
item_height=44: uniform rows. Offsets are exact and cheap, and native containers can skip estimating that extent.get_item_height=lambda item, i: ...: exact per-row extents without measurement. Native containers use those extents directly.- Nothing at all: rows start at
estimated_item_height(default 44) and are corrected with their measured extent once they've been on screen. Native containers refine their layout as measurements arrive; variable-height rows use the same recycling path as fixed-height rows.
separator_height= adds a fixed gap below every row.
Pull-to-refresh¶
Use RefreshControl as a prop on
either FlatList or ScrollView:
@pn.component
def Pullable():
refreshing, set_refreshing = pn.use_state(False)
def reload():
set_refreshing(True)
# ... fetch data ...
set_refreshing(False)
return pn.FlatList(
data=items,
item_height=44,
refresh_control=pn.RefreshControl(
refreshing=refreshing,
on_refresh=reload,
),
)
Row taps¶
Wrap the row in a Pressable inside
render_item:
def render_row(item, index):
return pn.Pressable(
pn.Text(item["title"]),
on_press=lambda: open_detail(item["id"]),
)
Infinite scroll¶
on_end_reached fires once when the user scrolls within
on_end_reached_threshold viewports of the end (re-arming when the
data length changes), which is the hook for pagination:
pn.FlatList(
data=items,
item_height=44,
on_end_reached=load_next_page,
on_end_reached_threshold=0.5, # half a viewport from the bottom
)
on_viewable_items_changed reports the set of visible rows whenever
it changes, as a list of {"index", "key", "item"} dicts.
Imperative scrolling¶
Pass a use_ref as ref= and the list
publishes a ListController on
ref.current:
@pn.component
def JumpableList():
list_ref = pn.use_ref()
return pn.Column(
pn.Button("Jump to row 200", on_press=lambda: list_ref.current.scroll_to_index(200)),
pn.FlatList(data=items, item_height=44, ref=list_ref, style={"flex": 1}),
style={"flex": 1},
)
The controller exposes scroll_to_index(i, animated=True),
scroll_to_offset(points, animated=True), and
scroll_to_end(animated=True).
Grids, headers, and empty states¶
num_columns=2chunks items into grid rows.horizontal=Truescrolls on the x-axis (extents become widths).list_header=/list_footer=render once before/after all rows.list_empty=renders whendatais empty.
Section lists¶
SectionList flattens an iterable of
{"title": ..., "data": [...]} sections into a single virtualized
list, dispatching to either render_section_header or render_item
depending on the row's kind.
sections = [
{"title": "A", "data": ["Apple", "Avocado"]},
{"title": "B", "data": ["Banana", "Blueberry"]},
]
pn.SectionList(
sections=sections,
item_height=44,
section_header_height=32,
render_section_header=lambda s, _: pn.Text(s["title"]),
render_item=lambda item, _i, _s: pn.Text(item),
)
Headers and items can have different extents, and variable-height
rows work exactly as in FlatList (exact via get_item_height, or
estimated and measured).
Performance notes¶
- Always provide a stable
key_extractorso rows that stay inside the window refresh in place rather than tearing down and rebuilding their subtree as the window shifts. - Provide real extents (
item_height/get_item_height) when you can. Exact extents reduce native measurement work. For variable content, choose anestimated_item_heightclose to typical row sizes. - Headers, footers, empty states, pull-to-refresh, grids, and horizontal scrolling all use native recycling on mobile. Test representative content and scrolling on your deployment targets.
- Keep row subtrees shallow. The reconciler is fast, but mounting a
hundred
Text/Image/Buttonnodes per row is wasteful work every time a row enters the window. - Move expensive computation out of
render_item(useuse_memoin the parent component, or pre-compute once before constructing the data list).