dioxus-compose guide

Architecture

Two halves, one narrow boundary

Rust does not call Compose. It describes what to draw as plain values, and a Kotlin interpreter on the other side turns them into Compose. Between them is a small C ABI: synchronous, on one thread, with nothing copied. Redwood and Jetpack Glance are built the same way.

Host and renderer

Host
The Rust side. It runs your components in a Dioxus VirtualDom, keeps your state, and for HTML screens computes styles and layout.
Renderer
Compose Multiplatform compiled ahead of time into a native library, with an interpreter for a fixed set of drawing elements and widgets. It owns the pixels, the text stack, the input method, scrolling and focus.
RepositoryHolds
dioxus-compose (this one)The Dioxus renderer: the adapter from Dioxus to compose-rust, and the HTML path (dioxus_compose::html)
compose-rustUsing Compose from Rust: the widget schema, the renderer and its builds, the boundary, the design systems

The split follows what each part is about. Turning CSS into drawing elements is about Dioxus and HTML, so it lives here; compose-rust never needs to know about it. Drawing those elements is about Compose, so it lives there, and compose-rust's own users get the same elements.

The HTML path

the HTML path
your component (rsx! with div, span, CSS)
  -> Dioxus VirtualDom                 runs it, produces mutations
  -> blitz-dom document                Stylo resolves CSS, Taffy lays out
       text sizes from a TextMeasurer
  -> DisplayList                       boxes in paint order, absolute rectangles
  -> Plan, and diff against the last   drawing elements nested in their containers
  -> renderer (Compose)                planned: the bridge is not built yet

Each stage is a value you can inspect, which is how the examples are tested. The DisplayList is what layout decided: rectangles, colours, borders, radii, shadows, clips, text runs, fields, images and scroll containers. The Plan is what the renderer will draw: boxes placed at offsets inside their parents, scroll containers holding their content, text, fields and images, with colours and brushes registered once. Its diff names what changed, node by node and modifier by modifier, so a change to one box's background sends one change.

CSS names and selectors never cross to the renderer. That keeps the renderer small and the same for every app, and it means a CSS feature is added in Rust, in one place.

Two pieces are not built yet. The bridge from the plan to compose-rust's AbsoluteBox and drawing modifiers is partial, and so is the synchronous call through which the Host will ask Compose how big a run of text is during layout (#43).

The widget path

For a screen written with widget names, the Host turns Dioxus's mutations into fixed-layout records (create, set a property, set a modifier, insert, move, remove, set text, append text) in one buffer per call. The renderer applies the whole buffer inside one Compose snapshot, so a half-applied tree is never drawn.

The boundary

The boundary is a handful of C functions, and adding one is a deliberate decision rather than a convenience. The model is JSI's rather than the old React Native bridge's: same thread, direct calls, synchronous return values.

The frame model

  1. The renderer receives input and calls into the Host on its UI thread.
  2. The Host runs your handler, diffs, encodes the changes and returns them with the handler's result, for example whether a key was consumed.
  3. The renderer applies the changes in one snapshot and reports the result to Compose.
  4. A worker thread that changed a signal causes a frame request. Requests coalesce into one frame on the next tick of Compose's frame clock.

The VirtualDom runs on the renderer's UI thread rather than a thread of its own, which removes the need for locks and queues. The only cross-thread signal in the design is the frame request. A frame requested while a handler runs waits until the handler returns, so the renderer is never re-entered from inside a call.

What runs where

The renderer is built per platform by compose-rust. Where Compose can be compiled for the platform directly, there is no virtual machine in the result.

PlatformRenderer built asCarries a Java runtime
macOSKotlin/NativeNo
LinuxKotlin/NativeNo
iOSKotlin/NativeNo
WebKotlin/WasmNo
AndroidKotlin on the platform's own runtimeThe platform's
WindowsGraalVM native imageYes, compiled in

Windows is the exception because Compose has no native target there that can be linked with Skia. The application does not know which it got: the boundary, the records and the interpreter are the same on every platform.