pub trait NativeComponent: 'static {
type Props: Clone + PartialEq + Send + 'static;
type State: 'static;
// Required methods
fn create(
&self,
ctx: &mut ComponentCtx<'_, '_, '_>,
props: &Self::Props,
) -> Option<(NativeRoot, Self::State)>;
fn update(
&self,
ctx: &mut ComponentCtx<'_, '_, '_>,
state: &mut Self::State,
old: &Self::Props,
new: &Self::Props,
);
// Provided methods
fn on_event(
&self,
state: &mut Self::State,
props: &Self::Props,
event: NativeEvent,
) -> Option<NativeEvent> { ... }
fn dispose(&self, ctx: &mut ComponentCtx<'_, '_, '_>, state: Self::State) { ... }
}Expand description
One retained native view — or native view hierarchy — created, updated and torn down entirely from Rust.
Implement this for a plain marker/config type, register it once under a
kind string (register_component), and the same runtime that serves this
crate’s built-in controls will serve yours: one generic platform
factory, one generic listener, no per-component Kotlin or Swift, ever.
Read the module doc first — it is the lifecycle contract (when each method runs, what the runtime guarantees about ordering, and what it does not guarantee about disposal promptness). The short version:
- every method runs on the platform main thread;
createruns at the host’s post-frame poll, a frame or more after the mounting widget appeared;updateruns only whenPropscompare unequal (plus the bounded retry window a failed one opens);on_eventruns from a platform listener between frames, for every view the component attached one to withComponentCtx::attach_listener(on_event);disposeis prompt on teardown but may be late, and at process exit may not run at all.
§No frust View children
A component’s native hierarchy is its own: frust sees one opaque slot
with one rect, and the platform (a LinearLayout, a UIStackView,
explicit frames) lays the subtree out. This is the SwiftUI
UIViewRepresentable / Compose AndroidView model, chosen deliberately
over React Native’s — where the framework’s own layout engine walks into
native containers — because frust’s wire carries no hierarchical child
geometry and buying one would mean re-acquiring, per child, the
frame-pairing, shield collection, culling and accessibility bridging it
gets per slot today.
Required Associated Types§
Sourcetype Props: Clone + PartialEq + Send + 'static
type Props: Clone + PartialEq + Send + 'static
The Rust-side-diffed create/update payload: everything the app tells this component, as one value it constructs directly.
PartialEq is load-bearing, not a formality — it is the gate that
keeps an unchanged rebuild from crossing the FFI boundary at all (a
comparison costs nanoseconds; a redundant JNI setter costs ~0.14–29 µs
depending on whether it re-layouts). Send is inherited from the
runtime’s internal contract; props never actually leave the main
thread.
Required Methods§
Sourcefn create(
&self,
ctx: &mut ComponentCtx<'_, '_, '_>,
props: &Self::Props,
) -> Option<(NativeRoot, Self::State)>
fn create( &self, ctx: &mut ComponentCtx<'_, '_, '_>, props: &Self::Props, ) -> Option<(NativeRoot, Self::State)>
Build this component’s native view (hierarchy) for a freshly attached slot, returning its root and the state that drives it.
Return None to fail the slot — the runtime then reports it dead to
the host rather than leaving a half-created control mounted. That is
the only failure channel create has, and it exists because there is
no meaningful State to hand back when the native side did not come
up; use ComponentCtx::report_error (or let a ctx helper latch its
own failure) first, so the log names what went wrong.
Returning Some after an error was latched is legal and means “I
recovered”: the slot lives and the latched error is logged as a
warning. The impl always has the final say.
Sourcefn update(
&self,
ctx: &mut ComponentCtx<'_, '_, '_>,
state: &mut Self::State,
old: &Self::Props,
new: &Self::Props,
)
fn update( &self, ctx: &mut ComponentCtx<'_, '_, '_>, state: &mut Self::State, old: &Self::Props, new: &Self::Props, )
Apply a props change to the live view with direct setters on the
handles State retained.
Called only when old != new (the module doc’s props diff gate), on
the main thread, ordered with this slot’s create/dispose.
A failure latched on ctx leaves the runtime’s diff baseline at old
and marks the slot for retry, so the next rebuild that publishes
this slot re-applies the change — even if the app’s props never differ
again. The trigger is a props-generation bump, not the app’s props
moving; the module doc’s A failed update is retried, up to a cap has
the mechanism and its limits (a retry needs a rebuild; three consecutive
failures spend the budget and the slot then stops asking, so a broken
component degrades to inert rather than to per-frame FFI traffic).
Provided Methods§
Sourcefn on_event(
&self,
state: &mut Self::State,
props: &Self::Props,
event: NativeEvent,
) -> Option<NativeEvent>
fn on_event( &self, state: &mut Self::State, props: &Self::Props, event: NativeEvent, ) -> Option<NativeEvent>
A platform listener the component attached fired for this slot: act on
it, and answer the event (if any) the app’s
NativeComponentView::on_event
hook should receive.
§Which events arrive here
Exactly the ones a listener this component attached reports: a view it
built (its root as much as a child) wired with
ComponentCtx::attach_listener, for the ListenerKinds it asked
for. The listener is the platform’s one shared class, bound to this
slot’s own id by the context — the component never sees the id, so it
cannot route anything but home — and its events reach this method
through the runtime’s slot-id routing, the path the built-in
controls’ events take. An event whose family this slot never attached
(a stray, or a hand-built Android listener carrying this slot’s number)
is dropped at the bridge and never reaches this method.
§The pair, the state and the props
event is the listener’s raw wire (NativeEvent::kind/
NativeEvent::detail, with NativeEvent::checked and
NativeEvent::value decoding the two payload-carrying kinds).
state is the component’s own, and props the last props a create
or successful update applied — the typed baseline the runtime diffs
against. The &self a dispatch runs against is the value the app
published this rebuild — re-read from the staging table on every
dispatch (the module doc’s point 5) — so it can carry the closures such
an event should reach.
§The answer
Some(event) forwards that event to the app’s hook (the builders’
events-as-signals idiom: the hook typically writes a signal, which wakes
exactly one frust frame); None swallows it. The answer need not be
the event that arrived — a component may translate one kind into
another — but only the kinds NativeEvent names are forwarded; any
other kind is dropped (logged at debug). The default forwards every
event unchanged, which is right for a component whose listeners exist
to report straight to the app.
It runs on the main thread inside the runtime’s borrow, like every listener dispatch here: a setter provoking its own listener synchronously from inside this method is dropped, not re-entered.
Sourcefn dispose(&self, ctx: &mut ComponentCtx<'_, '_, '_>, state: Self::State)
fn dispose(&self, ctx: &mut ComponentCtx<'_, '_, '_>, state: Self::State)
The slot is going away: detach listeners and release anything state
owns beyond the NativeRoot, which the runtime releases immediately
afterwards — the paired delete, in that order.
Defaults to doing nothing, which is correct whenever dropping State
already releases everything (the iOS arm’s Retained fields, an
Android state whose only refs are its own Globals).
§Which &self this runs against
The most recently published one — re-read from the staging table on
dispatch, not the value whose create produced state. Those differ
whenever a rebuild republished an equal Props with a different
component value (new closures, a different Rc): the props diff gate
skips update entirely on an equal-props rebuild, so without the
re-read this would run a stale rebuild’s closures.
One exception, and it is the common teardown path: when the mounting
widget’s on_cleanup has already reaped the staging entry, the
retained value is kept instead. That is correct — a torn-down widget
published nothing newer. The re-read matters for a dispose that
reaches a still-mounted slot: the differ’s missing-frame-streak
culling backstop, and suspend_all on surface teardown.
A ListenerHandle kept in state needs no call here: dropping it
with the state is its release (ListenerHandle’s own doc).
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".