Scroll and lists
Use Scroll for one composed child and a list component for large collections.
Native lists recycle platform rows and keep scrolling away from the PHP render
path.
ScrollView
Section titled “ScrollView”use Pam\Native\ScrollKeyboardDismissMode;use Pam\Native\UI\Column;use Pam\Native\UI\Scroll;
$content = Scroll::make( Column::make(...$sections),) ->showsIndicator(false) ->fillViewport() ->nestedScrollEnabled() ->keyboardDismissMode(ScrollKeyboardDismissMode::OnDrag) ->onScroll(fn ($event) => $this->rememberOffset($event));ScrollView::make($content) is the compatibility alias. Horizontal paging can
combine horizontal(), pagingEnabled(), snapToInterval() and a bounded
deceleration rate.
In a .pam.php template, declarative ScrollView accepts one or many direct
children. PAM Native 0.5.46 inserts a native Row for horizontal content and a
native Column for vertical content:
<ScrollView horizontal="true" showsHorizontalScrollIndicator="false"> <Pressable p-for="$story in $stories" :key="$story->id" width="66" > <Image :source="$story->avatar" width="66" height="66" /> <Text>{{ $story->name }}</Text> </Pressable></ScrollView>This keeps compact items at their authored width even when only one branch is
currently visible, while loops and conditions can safely produce multiple
items. The imperative Scroll::make($content) API remains explicit and still
accepts exactly one content element.
Imperative jumps and restoration
Section titled “Imperative jumps and restoration”PAM Native 0.5.29 adds tokenized scroll requests. A changed request number
executes exactly once on the native UI thread. PAM Native 0.5.60 also accepts
a persisted logical offset on that request. A testId has first priority, a
non-negative offset is second, and omitting both targets jumps to the content
end.
<ScrollView anchorToEnd="true" maintainVisibleContentPosition="true" :scrollTargetTestId="$scrollTarget" :scrollTargetOffset="$scrollTargetOffset" :scrollRequest="$scrollRequest"> <Column> <Row testId="first-unread"> <Text>Unread messages</Text> </Row> </Column></ScrollView>public string $scrollTarget = '';public float $scrollTargetOffset = -1.0;public int $scrollRequest = 0;
public function jumpToUnread(): void{ $this->scrollTarget = 'first-unread'; $this->scrollTargetOffset = -1.0; $this->scrollRequest++;}
public function restoreReadingPosition(float $offset): void{ $this->scrollTarget = ''; $this->scrollTargetOffset = max(0.0, $offset); $this->scrollRequest++;}
public function jumpToLatest(): void{ $this->scrollTarget = ''; $this->scrollTargetOffset = -1.0; $this->scrollRequest++;}The fluent PHP equivalent is
$scroll->scrollRequest($request, $targetTestId, $targetOffset). Keep the
request value in component state. The offset is expressed in logical points,
not physical pixels. Because it is part of a one-shot request, it restores
state without making the scroll continuously controlled or bouncing against
the user’s gesture.
anchorToEnd() controls initial placement and following new content while the
viewport is already near the end. maintainVisibleContentPosition() preserves
the visible region when older content is prepended. Scroll requests are
explicit user or application commands and take precedence when dispatched.
FlatList
Section titled “FlatList”FlatList is optimized for native text rows:
use Pam\Native\UI\FlatList;
$list = FlatList::make(array_map( fn ($contact) => $contact->displayName, $contacts,)) ->rowHeight(56) ->prefetch(8) ->initialScrollIndex($restoredIndex) ->removeClippedSubviews() ->onEndReached(fn () => $this->loadNextPage(), threshold: 0.35);VirtualizedList and SectionList
Section titled “VirtualizedList and SectionList”use Pam\Native\UI\SectionList;use Pam\Native\UI\Text;use Pam\Native\UI\VirtualizedList;
$cards = VirtualizedList::make( ...array_map( fn ($item) => ContactCard::make($item)->key("contact:{$item->id}"), $contacts, ),) ->estimatedRowHeight(72) ->prefetch(10) ->onEndReached(fn () => $this->loadMore());
$sections = SectionList::make([ ['title' => 'Favorites', 'items' => ['Ada', 'Linus']], ['title' => 'Recent', 'items' => ['Margaret', 'James']],])->rowHeight(52);Use stable keys for composed rows. VirtualGrid applies the same recycled
list contract to multi-column responsive cells.
For rich cells, estimatedRowHeight is a prefetch estimate and the fallback
when a cell has no authored main-axis size. Explicit cell heights remain
authoritative:
<VirtualizedList estimatedRowHeight="604" prefetch="4"> <Column p-for="$post in $posts" :key="$post['id']" :height="$post['rowHeight']" > <Image :source="$post['image']" widthPercent="100" :height="$post['mediaHeight']" /> </Column></VirtualizedList>PAM computes these extents in Rust and gives each recycled Android holder its
exact native size. Scrolling does not synchronously measure PHP components on
the UI thread. Horizontal rich lists use an explicit cell width the same
way, while rowHeight remains a compatibility alias.
The list viewport and each recycled holder are hard native paint boundaries.
Media cannot draw over headers, neighboring rows, or tab bars while scrolling.
Inside the cell, component-level overflow still controls descendant clipping.
Pull to refresh
Section titled “Pull to refresh”use Pam\Native\RefreshIndicatorSize;use Pam\Native\UI\RefreshControl;
$feed = RefreshControl::make($cards, $this->state->refreshing) ->colors(0xFF2563EB, 0xFF7C3AED) ->progressBackgroundColor(0xFFFFFFFF) ->size(RefreshIndicatorSize::Default) ->onRefresh(fn () => $this->refresh());Avoid rendering an unbounded collection into a Column inside Scroll.
Choose a recycled list whenever item count can grow with remote or user data.
Android has the mature recycled-list path; confirm the current iOS parity
record before relying on identical large-list behavior.