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: sharedViewHandlerprotocol 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
¶
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_for |
Return the handler registered for |
resolve_view |
Return the native view registered under |
record_for |
Return the full |
live_view_count |
Number of views currently tracked (test/diagnostic helper). |
apply_mutations |
Apply one commit transaction. |
measure_intrinsic |
Return the natural |
command |
Execute an imperative command against the view for |
set_animated_property |
Apply one Python-driven animation frame to the view for |
start_animation |
Start a natively-driven animation on the view for |
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., |
required |
handler
|
ViewHandler
|
A |
required |
handler_for
¶
handler_for(type_name: str) -> Optional[ViewHandler]
Return the handler registered for type_name, if any.
live_view_count
¶
live_view_count() -> int
Number of views currently tracked (test/diagnostic helper).
apply_mutations
¶
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
¶
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
¶
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.
|
required |
args
|
Optional[Dict[str, Any]]
|
Optional command arguments. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The handler's command result, or |
Any
|
unknown. |
set_animated_property
¶
Apply one Python-driven animation frame to the view for tag.
start_animation
¶
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 |
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 |
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 |
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):
- A
CreateOpfor a tag precedes any other op referencing that tag. InsertOpops appear after both the parent and child exist.DestroyOpops appear after the correspondingRemoveOp(if the node was attached) and are emitted children-first.SetFrameOpops are only emitted for frames that actually changed since the last layout pass (frame diffing).
Classes:
| Name | Description |
|---|---|
CreateOp |
Create a native view for |
UpdateOp |
Apply |
InsertOp |
Ensure the child view sits at |
RemoveOp |
Detach the child view from the parent view (without destroying it). |
DestroyOp |
Release the native view registered under |
SetFrameOp |
Position and size the view registered under |
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
¶
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. |
props |
Dict[str, Any]
|
Initial clean props; callables have already been
routed to the |
UpdateOp
dataclass
¶
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
¶
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
¶
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.
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_eventwith 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 |
Functions:
| Name | Description |
|---|---|
get_event_registry |
Return the process-wide |
dispatch_event |
Dispatch an event from a native view into Python. |
extract_events |
Split |
event_names |
Return the event-name set a handler should consult for |
Attributes:
| Name | Type | Description |
|---|---|---|
EVENTS_PROP |
Prop key carrying the |
|
GESTURES_PROP |
Prop key carrying gesture descriptors (see |
EVENTS_PROP
module-attribute
¶
Prop key carrying the frozenset of event names wired on an element.
GESTURES_PROP
module-attribute
¶
Prop key carrying gesture descriptors (see pythonnative.gestures).
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 |
clear |
Drop every registration for |
get |
Return the callback for |
has |
Return whether a callback is registered for |
dispatch |
Invoke the callback for |
reset |
Drop every registration (test helper). |
dispatch_event
¶
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 ( |
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 |
extract_events
¶
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_controldicts have their nestedon_refreshhoisted to the"on_refresh"event; the remaining keys (refreshing,tint_color) stay in the payload.gestureslists 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]
|
|
Dict[str, Callable[..., Any]]
|
callables and |
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 |
remove_child |
Remove |
destroy |
Release platform resources owned by |
set_frame |
Position and size |
measure_intrinsic |
Return the natural |
command |
Execute an imperative command (e.g. |
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 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
|
required |
Returns:
| Type | Description |
|---|---|
Any
|
The platform-native view object. |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
Subclasses must override. |
update
¶
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
|
required |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
Subclasses must override. |
insert_child
¶
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 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
¶
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
¶
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 |
required |
max_height
|
float
|
Maximum height in points (or |
required |
Returns:
| Type | Description |
|---|---|
Tuple[float, float]
|
|
command
¶
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
¶
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 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
|
|
bool
|
tells the caller to fall back to the Python ticker (the |
bool
|
default). |
parse_color_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 |
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¶
- Read the high-level model in Native views (concept).
- See how the reconciler drives handlers in Reconciler.