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.
Leading and trailing spacers stand in for everything off-screen, and
the window of mounted rows shifts as the user scrolls. The windowing
runs in Python on top of the platform's native scroll view, so the
same behavior (including on_end_reached, viewability callbacks, and
the imperative scroll controller) is identical on Android, iOS, and
the desktop preview.
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.
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.get_item_height=lambda item, i: ...: exact per-row extents without measurement.- Nothing at all: rows start at
estimated_item_height(default 44) and are corrected with their measured extent once they've been on screen. Scroll positions stay stable as estimates converge.
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 dict as ref= and the list
attaches a scroll controller:
@pn.component
def JumpableList():
list_ref = pn.use_ref()
return pn.Column(
pn.Button("Jump to row 200", on_click=lambda: list_ref["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; measured estimation works but does more bookkeeping per scroll. - 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).