State and lifecycle
PAM keeps the PHP runtime and component instances alive. State mutations schedule a new render, Rust compares the resulting element tree, and each platform renderer receives only the necessary mutation batch.
Local state
Section titled “Local state”Single-file components can mark mutable properties with #[State]:
final class Counter extends Component{ #[State] public int $count = 0;
public function increment(): void { $this->count++; }}State belongs to a stable component identity. Reordering repeated children without keys may transfer identity to the wrong item. Use a stable key whenever children can be inserted, removed, sorted, or moved.
Public constructor parameters are component props. PAM updates mutable public props on a stable child instance.
A private or readonly constructor value cannot be updated safely. When it changes, PAM remounts that child intentionally.
Lifecycle
Section titled “Lifecycle”Components may react to mount, updated props/state, and unmount through the component lifecycle contract. Keep lifecycle work bounded:
- start subscriptions or resources at mount;
- update only when relevant values changed;
- release resources at unmount;
- cancel late asynchronous work; and
- never manipulate Android Views from the PHP worker.
Render and commit path
Section titled “Render and commit path”PHP component state │ render ▼Element tree │ PNT1 first frame / PNP1 updates ▼Rust reconciliation and layout │ PNB1 mutation batch ▼Kotlin or Swift frame queue │ one UI commit per VSYNC ▼Android Views or UIKit controlsThe first render is a complete tree. Later renders use numeric create, remove, property-update, move, and set-root operations.
Node IDs are stable 64-bit hashes of keyed PHP identities. Reused immutable subtrees are cached through WeakMap; returning the same tree can produce no native commit.
Layout invalidation
Section titled “Layout invalidation”Paint-only changes can skip layout. Dimensions, flex rules, gaps, margins, min/max constraints, and alignment invalidate the relevant layout path.
The renderer applies only layout IDs emitted by Rust instead of rescanning every Android View.
Native-owned interaction
Section titled “Native-owned interaction”High-frequency interaction remains native on both renderers:
- input text, cursor, and selection;
- press feedback and long-press recognition;
- scroll and fling progress;
- list recycling and prefetch on Android;
- modal and navigation animations;
- image fetch, decode, and caching;
- safe-area and IME calculations; and
- transform and opacity animations.
PHP receives semantic events according to the chosen synchronization policy. On iOS, scroll, dimensions, image progress, input selection, input content size, and pointer-move events are coalesced before the next CADisplayLink commit.
Input synchronization
Section titled “Input synchronization”Text remains in the native input while the user types. Change delivery defaults to a 48 ms debounce and can be configured as:
| Mode | PHP synchronization |
|---|---|
| Native | No per-keystroke state synchronization |
| Debounced | Coalesced after a short quiet period |
| Immediate | Every accepted change |
| Blur | When the input loses focus |
| Submit | When the input submits |
Selection events are coalesced to VSYNC. Key, content-size, end-editing, and other detailed events are opt-in.
iOS advanced event lifecycle
Section titled “iOS advanced event lifecycle”The Swift renderer installs and removes event bridges with the node lifecycle:
onPressIn,onPressOut, andonPressMoveuse one zero-delay native recognizer;onInputEndEditing,onInputSelectionChange,onInputContentSizeChange, andonInputKeyPressstay attached toPamInputField;- modal
requestClose,show,dismiss, andorientationChangecallbacks are detached when the node is removed; - image loading emits start, progress, load or error, and load-end in lifecycle order;
- refresh and drawer callbacks are owned by their native containers; and
- scroll listeners support
onScrollandonEndReachedindependently.
onEndReached uses property ID 84 as a non-negative viewport-relative threshold, defaults to 0.5, supports both axes, emits once for a content/viewport state, and rearms when either size changes.
Cleanup and buffer ownership
Section titled “Cleanup and buffer ownership”Rust-owned PNB1 buffers cross JNI through direct, read-only ByteBuffer instances. Kotlin accepts ownership, applies the batch, and releases the buffer. Shutdown drains queued and accepted buffers before destroying the renderer.
Debug output exposes the number of retained buffers; it must return to zero after a completed frame.