Skip to content

Native views

The bridge between PythonNative's element tree and concrete native widgets. Each commit's diff is expressed as a flat list of mutation ops referencing integer tags, applied through a single apply_mutations call. Every element type maps to a ViewHandler implementation in the NativeViewRegistry; the platform-specific handlers are registered lazily so importing pythonnative on the desktop never pulls in Chaquopy or rubicon-objc.

Platform-specific native-view creation and update logic.

This package provides the NativeViewRegistry that maps element type names (e.g., "Text", "Button") to platform-specific ViewHandler implementations, and owns the tag table mapping each reconciler-assigned integer tag to its live native view.

The reconciler communicates exclusively through apply_mutations: one ordered batch of create/update/insert/remove/destroy/frame ops per commit (see pythonnative.mutations). Imperative escape hatches (commands, animation control, intrinsic measurement) resolve views through the same tag table.

Platform handlers live in dedicated submodules:

  • pythonnative.native_views.base: shared ViewHandler protocol and utilities.
  • pythonnative.native_views.android: Android handlers (Chaquopy / Java bridge).
  • pythonnative.native_views.ios: iOS handlers (rubicon-objc).
  • pythonnative.native_views.desktop: Tkinter preview handlers.

All platform-branching is handled at registration time via lazy imports, so this package can be imported on any platform for testing. A mock registry can be installed via set_registry to drive the reconciler with no real native views.

Modules:

Name Description
android

Android native-view handlers (Chaquopy / Java bridge).

base

Shared base classes and utilities for native-view handlers.

desktop

Desktop native-view handlers (Tkinter).

ios

iOS native-view handlers (rubicon-objc).

Classes:

Name Description
ViewRecord

One live native view tracked by the tag table.

NativeViewRegistry

Map element type names to handlers and tags to live native views.

Functions:

Name Description
get_registry

Return the process-wide registry, lazily registering handlers.

refresh_registry

Re-run SDK handler installation against the existing registry.

set_registry

Install a custom registry (primarily for testing).

ViewRecord

ViewRecord(tag: int, type_name: str, view: Any, handler: ViewHandler)

One live native view tracked by the tag table.

NativeViewRegistry

NativeViewRegistry()

Map element type names to handlers and tags to live native views.

The reconciler depends only on this protocol: apply_mutations, resolve_view, measure_intrinsic, and command. Implementations may host real platform handlers (Android/iOS/ desktop) or mocks for tests.

Methods:

Name Description
register

Register handler to service elements of type type_name.

handler_for

Return the handler registered for type_name, if any.

resolve_view

Return the native view registered under tag, or None.

record_for

Return the full ViewRecord for tag.

live_view_count

Number of views currently tracked (test/diagnostic helper).

apply_mutations

Apply one commit transaction.

measure_intrinsic

Return the natural (width, height) of a content-sized view.

command

Execute an imperative command against the view for tag.

set_animated_property

Apply one Python-driven animation frame to the view for tag.

start_animation

Start a natively-driven animation on the view for tag.

cancel_animation

Cancel a natively-driven animation; returns the presentation value if known.

register

register(type_name: str, handler: ViewHandler) -> None

Register handler to service elements of type type_name.

Parameters:

Name Type Description Default
type_name str

The element type name (e.g., "Text").

required
handler ViewHandler

A ViewHandler instance for the active platform.

required

handler_for

handler_for(type_name: str) -> Optional[ViewHandler]

Return the handler registered for type_name, if any.

resolve_view

resolve_view(tag: int) -> Any

Return the native view registered under tag, or None.

record_for

record_for(tag: int) -> Optional[ViewRecord]

Return the full ViewRecord for tag.

live_view_count

live_view_count() -> int

Number of views currently tracked (test/diagnostic helper).

apply_mutations

apply_mutations(ops: Sequence[Mutation]) -> None

Apply one commit transaction.

Ops are applied strictly in order. Failures are isolated per op: a handler exception is logged (rate-limited) and the remaining ops still apply, so one bad prop can't desync the whole native tree.

Parameters:

Name Type Description Default
ops Sequence[Mutation]

Ordered mutations emitted by the reconciler.

required

measure_intrinsic

measure_intrinsic(tag: int, max_width: float, max_height: float) -> Tuple[float, float]

Return the natural (width, height) of a content-sized view.

Used by the layout engine for leaves whose intrinsic size depends on their content (text, buttons, images).

command

command(tag: int, name: str, args: Optional[Dict[str, Any]] = None) -> Any

Execute an imperative command against the view for tag.

Parameters:

Name Type Description Default
tag int

Target view tag.

required
name str

Command name (handler-specific, e.g. "scroll_to_offset").

required
args Optional[Dict[str, Any]]

Optional command arguments.

None

Returns:

Type Description
Any

The handler's command result, or None when the tag is

Any

unknown.

set_animated_property

set_animated_property(tag: int, prop_name: str, value: Any) -> None

Apply one Python-driven animation frame to the view for tag.

start_animation

start_animation(tag: int, anim_id: int, prop_name: str, spec: Dict[str, Any]) -> bool

Start a natively-driven animation on the view for tag.

Returns:

Type Description
bool

Whether the platform accepted the animation (False

bool

means the caller should drive it from the Python ticker).

cancel_animation

cancel_animation(tag: int, anim_id: int) -> Any

Cancel a natively-driven animation; returns the presentation value if known.

get_registry

get_registry() -> NativeViewRegistry

Return the process-wide registry, lazily registering handlers.

The first call instantiates the registry, registers either the Android or iOS handlers based on IS_ANDROID, then layers on every decorator-registered SDK handler (and any handlers exposed by third-party packages via the pythonnative.handlers entry point group). Subsequent calls return the same instance.

Returns:

Type Description
NativeViewRegistry

The active NativeViewRegistry.

refresh_registry

refresh_registry() -> NativeViewRegistry

Re-run SDK handler installation against the existing registry.

Call this after registering a new component at runtime if the registry has already been instantiated. This is mostly useful in REPL sessions and tests; the normal flow is "register, then call get_registry" and the handlers come along automatically.

Returns:

Type Description
NativeViewRegistry

The active NativeViewRegistry.

set_registry

set_registry(registry: Optional[NativeViewRegistry]) -> None

Install a custom registry (primarily for testing).

Replaces the lazy singleton so subsequent get_registry calls return registry. Pass a mock to drive the reconciler from unit tests without touching real native APIs. Pass None to reset the singleton; the next get_registry call will then rebuild it from scratch.

Parameters:

Name Type Description Default
registry Optional[NativeViewRegistry]

The replacement registry, or None to clear.

required

Mutation ops

Batched mutation protocol between the reconciler and native backends.

The reconciler no longer talks to the native layer one call at a time. Instead, every commit pass produces an ordered list of small mutation ops referencing integer tags (stable per-view identifiers), and the whole list is applied in a single apply_mutations call. This mirrors React Native's Fabric mounting layer: the diff phase is pure, and the native side sees one coherent transaction per commit.

Why tags instead of view objects?

  • The diff phase runs before any native view exists, so ops cannot reference views directly.
  • Tags give the native side a stable identity to key its own view registry, event routing, and animation bookkeeping on.
  • A flat list of (op, tag, payload) tuples is trivially serializable, which keeps the door open for applying mutations from a background thread or through a single JNI/ObjC crossing in the future.

Op ordering rules (the reconciler guarantees these):

  1. A CreateOp for a tag precedes any other op referencing that tag.
  2. InsertOp ops appear after both the parent and child exist.
  3. DestroyOp ops appear after the corresponding RemoveOp (if the node was attached) and are emitted children-first.
  4. SetFrameOp ops are only emitted for frames that actually changed since the last layout pass (frame diffing).

Classes:

Name Description
CreateOp

Create a native view for tag of element type type_name.

UpdateOp

Apply changed_props to the view registered under tag.

InsertOp

Ensure the child view sits at index inside the parent view.

RemoveOp

Detach the child view from the parent view (without destroying it).

DestroyOp

Release the native view registered under tag.

SetFrameOp

Position and size the view registered under tag.

Attributes:

Name Type Description
Mutation

Union of every op type carried by a commit transaction.

Mutation module-attribute

Union of every op type carried by a commit transaction.

CreateOp dataclass

CreateOp(tag: int, type_name: str, props: Dict[str, Any] = dict())

Create a native view for tag of element type type_name.

Attributes:

Name Type Description
tag int

Unique integer identity assigned by the reconciler.

type_name str

Element type name (e.g. "Text").

props Dict[str, Any]

Initial clean props; callables have already been routed to the EventRegistry and replaced by the _pn_events name set.

UpdateOp dataclass

UpdateOp(tag: int, changed_props: Dict[str, Any] = dict())

Apply changed_props to the view registered under tag.

Removed props are signaled with a value of None, matching the pre-existing handler contract.

InsertOp dataclass

InsertOp(parent_tag: int, child_tag: int, index: int)

Ensure the child view sits at index inside the parent view.

Handlers must treat this as move-aware: if the child is already attached to the parent at a different position, it is moved rather than duplicated. index is clamped by handlers to the current child count.

RemoveOp dataclass

RemoveOp(parent_tag: int, child_tag: int)

Detach the child view from the parent view (without destroying it).

DestroyOp dataclass

DestroyOp(tag: int)

Release the native view registered under tag.

The registry drops its tag record and calls the handler's destroy hook so platform resources (listeners, timers, image loads) can be released eagerly instead of waiting for GC.

SetFrameOp dataclass

SetFrameOp(tag: int, x: float, y: float, width: float, height: float)

Position and size the view registered under tag.

Coordinates are points relative to the parent's content origin, exactly as computed by the layout engine.

Attributes:

Name Type Description
frame Tuple[float, float, float, float]

Return (x, y, width, height) as a tuple.

frame property

Return (x, y, width, height) as a tuple.

Event routing

Tag-based event routing between native views and Python callbacks.

Before the batched-commit overhaul, every event prop (on_click, on_change, …) was wired by storing the Python callable on (or next to) the native view, and every re-render re-pushed fresh closures across the bridge. This module replaces that with a single dispatch channel:

  • The reconciler strips callable props out of the payload sent to native handlers and registers them here, keyed by (tag, name).
  • Handlers wire their platform listener once at view creation; the listener calls dispatch_event with the view's tag and the event name.
  • Re-renders only mutate this Python-side registry; no native call is made when just a callback identity changes.

The set of event names present on an element is forwarded to handlers under the EVENTS_PROP key (a frozenset), so handlers that wire expensive listeners (scroll delegates, gesture recognizers) can do so conditionally. Dispatching an event nobody listens to is a cheap dict miss.

Classes:

Name Description
EventRegistry

Process-wide map of (tag, event name) -> Python callback.

Functions:

Name Description
get_event_registry

Return the process-wide EventRegistry.

dispatch_event

Dispatch an event from a native view into Python.

extract_events

Split props into native-safe props and Python event callbacks.

event_names

Return the event-name set a handler should consult for props.

Attributes:

Name Type Description
EVENTS_PROP

Prop key carrying the frozenset of event names wired on an element.

GESTURES_PROP

Prop key carrying gesture descriptors (see pythonnative.gestures).

EVENTS_PROP module-attribute

EVENTS_PROP = '_pn_events'

Prop key carrying the frozenset of event names wired on an element.

GESTURES_PROP module-attribute

GESTURES_PROP = 'gestures'

Prop key carrying gesture descriptors (see pythonnative.gestures).

EventRegistry

EventRegistry()

Process-wide map of (tag, event name) -> Python callback.

Thread-safe: native backends may dispatch from the platform UI thread while the reconciler updates registrations from the render thread.

Methods:

Name Description
set_events

Replace every registration for tag with events.

clear

Drop every registration for tag (called on view destroy).

get

Return the callback for (tag, name), or None.

has

Return whether a callback is registered for (tag, name).

dispatch

Invoke the callback for (tag, name) with args.

reset

Drop every registration (test helper).

set_events

set_events(tag: int, events: Dict[str, Callable[..., Any]]) -> None

Replace every registration for tag with events.

clear

clear(tag: int) -> None

Drop every registration for tag (called on view destroy).

get

get(tag: int, name: str) -> Optional[Callable[..., Any]]

Return the callback for (tag, name), or None.

has

has(tag: int, name: str) -> bool

Return whether a callback is registered for (tag, name).

dispatch

dispatch(tag: int, name: str, *args: Any) -> bool

Invoke the callback for (tag, name) with args.

Returns:

Type Description
bool

True when a callback existed and was invoked (even if

bool

it raised: exceptions are swallowed so a buggy app

bool

callback can't crash the platform's UI thread), False

bool

when nothing is registered.

reset

reset() -> None

Drop every registration (test helper).

get_event_registry

get_event_registry() -> EventRegistry

Return the process-wide EventRegistry.

dispatch_event

dispatch_event(tag: int, name: str, *args: Any) -> bool

Dispatch an event from a native view into Python.

This is the single entry point platform handlers call when a native listener fires.

Parameters:

Name Type Description Default
tag int

The view's reconciler-assigned tag.

required
name str

Event name, the original prop name ("on_click", "on_change", …) or a gesture channel ("gesture:0").

required
*args Any

Positional arguments forwarded to the user callback, preserving each prop's documented signature.

()

Returns:

Type Description
bool

Whether a callback was registered for (tag, name).

extract_events

extract_events(props: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Callable[..., Any]]]

Split props into native-safe props and Python event callbacks.

Rules:

  • Top-level callables named on_* become events under their prop name and are removed from the native payload.
  • refresh_control dicts have their nested on_refresh hoisted to the "on_refresh" event; the remaining keys (refreshing, tint_color) stay in the payload.
  • gestures lists of gesture descriptors are serialized to plain dicts (handlers wire recognizers from them) while their callbacks are folded into per-gesture "gesture:<i>" routers.
  • The resulting payload carries _pn_events (a frozenset of the event names present), so handlers can wire listeners conditionally and the prop differ can detect listener addition/removal without comparing closures.

Parameters:

Name Type Description Default
props Dict[str, Any]

Raw element props (already stripped of reconciler-owned keys).

required

Returns:

Type Description
Dict[str, Any]

(clean_props, events) where clean_props contains no

Dict[str, Callable[..., Any]]

callables and events maps event names to callbacks.

event_names

event_names(props: Dict[str, Any]) -> FrozenSet[str]

Return the event-name set a handler should consult for props.

Base classes

Shared base classes and utilities for native-view handlers.

Provides the ViewHandler protocol implemented by Android and iOS handlers, plus the parse_color_int helper shared across platforms.

Layout itself is not a handler responsibility. The pure-Python flex engine in pythonnative.layout owns sizing and positioning; handlers receive computed frames via set_frame and optionally expose an intrinsic-size hook via measure_intrinsic for content-sized leaves (text, buttons, images).

Classes:

Name Description
ViewHandler

Protocol implemented by every native-view handler.

Functions:

Name Description
parse_color_int

Parse a color value into a signed 32-bit ARGB int.

ViewHandler

Protocol implemented by every native-view handler.

A ViewHandler knows how to create, update, re-parent, and destroy native views of one element type. The reconciler never calls a handler directly; it emits a batch of mutation ops (pythonnative.mutations) that the NativeViewRegistry applies by dispatching to handlers. Handlers never need to know about Element or VNode.

Event contract: props delivered to create / update contain no Python callables. The set of event names wired on the element arrives under the _pn_events key (see event_names); handlers wire platform listeners once at create time and forward firings through dispatch_event using the tag passed to create.

Subclasses must override create and update. Container handlers override the child-management methods; leaf handlers can leave them as no-ops. Handlers whose intrinsic size depends on content (text, buttons, images) override measure_intrinsic.

Methods:

Name Description
create

Create a fresh native view and apply initial visual props.

update

Apply only the visual props that changed since the last render.

insert_child

Ensure child sits at index among parent's children.

remove_child

Remove child from parent without destroying it. No-op for leaf handlers.

destroy

Release platform resources owned by native_view.

set_frame

Position and size native_view relative to its parent.

measure_intrinsic

Return the natural (width, height) of a content-sized view.

command

Execute an imperative command (e.g. "scroll_to_offset").

set_animated_property

Apply one frame of a Python-driven animation immediately.

start_animation

Start a natively-driven animation, if the platform supports it.

cancel_animation

Cancel a natively-driven animation.

create

create(tag: int, props: Dict[str, Any]) -> Any

Create a fresh native view and apply initial visual props.

Layout-related props (width, height, flex, padding, etc.) are consumed by the layout engine and applied via set_frame, so handlers should ignore them here.

Parameters:

Name Type Description Default
tag int

The reconciler-assigned identity for this view. Used when dispatching events back into Python.

required
props Dict[str, Any]

Initial props dict (callable-free; event names under _pn_events).

required

Returns:

Type Description
Any

The platform-native view object.

Raises:

Type Description
NotImplementedError

Subclasses must override.

update

update(native_view: Any, changed_props: Dict[str, Any]) -> None

Apply only the visual props that changed since the last render.

Parameters:

Name Type Description Default
native_view Any

The platform-native view to mutate.

required
changed_props Dict[str, Any]

Props whose values changed (a value of None indicates the prop was removed).

required

Raises:

Type Description
NotImplementedError

Subclasses must override.

insert_child

insert_child(parent: Any, child: Any, index: int) -> None

Ensure child sits at index among parent's children.

Must be move-aware: when child is already attached to parent, reposition it instead of attaching twice. Handlers should clamp index to the current child count. No-op for leaf handlers.

remove_child

remove_child(parent: Any, child: Any) -> None

Remove child from parent without destroying it. No-op for leaf handlers.

destroy

destroy(native_view: Any) -> None

Release platform resources owned by native_view.

Called exactly once when the reconciler unmounts the view. The default is a no-op; override to detach listeners, cancel in-flight work, or destroy widgets that the platform doesn't garbage-collect.

set_frame

set_frame(native_view: Any, x: float, y: float, width: float, height: float) -> None

Position and size native_view relative to its parent.

Coordinates are in points and relative to the parent's content origin. Default no-op so handlers that don't need explicit positioning (e.g., Modal) can opt out.

Parameters:

Name Type Description Default
native_view Any

The platform-native view.

required
x float

X-coordinate (points) of the view's top-left corner relative to its parent's content origin.

required
y float

Y-coordinate (points) of the view's top-left corner.

required
width float

View width in points.

required
height float

View height in points.

required

measure_intrinsic

measure_intrinsic(native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]

Return the natural (width, height) of a content-sized view.

Used by the layout engine for leaves whose size depends on their content (text, buttons, images). Either max_width or max_height may be math.inf to indicate no constraint.

The default implementation returns (0, 0); override for leaves whose size depends on their content. Container handlers leave this alone; the engine sizes containers by laying out their children.

Parameters:

Name Type Description Default
native_view Any

The platform-native view to measure.

required
max_width float

Maximum width in points (or math.inf).

required
max_height float

Maximum height in points (or math.inf).

required

Returns:

Type Description
Tuple[float, float]

(width, height) in points.

command

command(native_view: Any, name: str, args: Dict[str, Any]) -> Any

Execute an imperative command (e.g. "scroll_to_offset").

Commands are the escape hatch for one-shot imperative actions that don't fit declarative props: scrolling, focusing, flashing indicators. Unknown commands should be ignored.

Parameters:

Name Type Description Default
native_view Any

The platform-native view.

required
name str

Command name.

required
args Dict[str, Any]

Command arguments.

required

Returns:

Type Description
Any

An optional command-specific result.

set_animated_property

set_animated_property(native_view: Any, prop_name: str, value: Any) -> None

Apply one frame of a Python-driven animation immediately.

This is the fallback path used by the desktop preview and by animations the platform cannot drive natively. prop_name is one of opacity, background_color, translate_x, translate_y, scale, scale_x, scale_y, rotate.

start_animation

start_animation(native_view: Any, anim_id: int, prop_name: str, spec: Dict[str, Any]) -> bool

Start a natively-driven animation, if the platform supports it.

spec describes the animation::

{"kind": "timing", "from": 0.0, "to": 1.0,
 "duration_ms": 300.0, "easing": "ease_in_out"}
{"kind": "spring", "from": ..., "to": ...,
 "stiffness": 100.0, "damping": 10.0, "mass": 1.0,
 "initial_velocity": 0.0}

Implementations must invoke pythonnative.animated.native_animation_completed(anim_id, finished) when the animation completes or is cancelled.

Returns:

Type Description
bool

True when the animation was started natively. False

bool

tells the caller to fall back to the Python ticker (the

bool

default).

cancel_animation

cancel_animation(native_view: Any, anim_id: int) -> Any

Cancel a natively-driven animation.

Returns:

Type Description
Any

The property's current (presentation) value when the

Any

platform can read it, else None.

parse_color_int

parse_color_int(color: Union[str, int]) -> int

Parse a color value into a signed 32-bit ARGB int.

Accepts "#RRGGBB", "#AARRGGBB", or a raw integer. Java APIs such as setBackgroundColor expect a signed 32-bit int, so values with a high alpha byte (e.g., 0xFF......) must be converted to their negative two's-complement equivalent.

Parameters:

Name Type Description Default
color Union[str, int]

Hex string (with or without leading #) or an int.

required

Returns:

Type Description
int

Signed 32-bit ARGB int suitable for Android's color APIs.

Platform handlers

The Android and iOS handler implementations live in pythonnative.native_views.android and pythonnative.native_views.ios respectively. They are imported only at runtime on the corresponding platform; we don't render their API tables here because they're internal to the runtime and require platform-only dependencies (Chaquopy / rubicon-objc) to be importable for mkdocstrings to introspect them.

Next steps