Skip to main content

NativeComponent

Trait NativeComponent 

Source
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;
  • create runs at the host’s post-frame poll, a frame or more after the mounting widget appeared;
  • update runs only when Props compare unequal (plus the bounded retry window a failed one opens);
  • on_event runs from a platform listener between frames, for every view the component attached one to with ComponentCtx::attach_listener (on_event);
  • dispose is 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§

Source

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.

Source

type State: 'static

Per-instance retained state: native handles, listeners, buffers — whatever create needs to keep to drive the view later. Created at attach time and held by the runtime’s slot registry, dropped immediately after dispose returns.

Required Methods§

Source

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.

Source

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§

Source

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.

Source

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".

Implementors§