Android Host Engine
viewcompose-host-android is the low-level Android View host engine. It installs the renderer,
owns retained render sessions, schedules invalidations on Choreographer frames, bridges Android
saved state and environment values, adapts focus/logging/tracing, offers neutral overlay discovery
for custom low-level hosts, and exposes native View, animation, and graphics interop. It deliberately does not
own Activity/Fragment convenience entry points, Material theme resolution, Lifecycle locals, or
ViewModel locals.
Applications should normally depend on viewcompose-android.
Depend on this artifact directly only when building a custom container host or using its interop
APIs without the standard Activity/Fragment integration.
Artifact and stability
dependencies {
implementation("com.viewcompose:viewcompose-host-android:0.1.0-alpha04")
}
- Stability: Alpha.
- Platform: Android library,
minSdk 24,compileSdk 36, and Java 11 bytecode. - API dependencies: runtime, UI contract, UI foundation, AndroidX Lifecycle, and AndroidX SavedState where their types appear in public signatures.
- Private implementation dependencies: Android renderer, coroutines Android, ConstraintLayout, and DynamicAnimation.
- Material Components is not a dependency of this module.
The module exclusively owns com.viewcompose.host.android. Activity and Fragment composition
roots use com.viewcompose.android and therefore cannot silently expand this low-level package.
Custom container hosting
renderInto(container) installs the Android engine and commits the first frame before returning:
fun renderIntoSample(container: ViewGroup) {
val diagnostics = RenderDiagnostics(
collection = RenderDiagnosticCollection(
frameLevel = RenderFrameDiagnosticLevel.Stats,
),
sink = { event -> println(event) },
)
val session = renderInto(container, diagnostics = diagnostics) {
Text("Custom host")
}
session.setRenderingActive(false)
session.render()
session.dispose()
check(runCatching(session::render).exceptionOrNull() is IllegalStateException)
}
Pass diagnostics = RenderDiagnostics(...) to start a correlated diagnostics tree. The low-level
role and parentLocalSnapshot parameters are Q3 integration controls for independently rendered
children; ordinary custom roots keep the Host default and no parent snapshot. debug controls
logging and slow-operation warnings, not diagnostics collection.
This low-level entry does not automatically provide Lifecycle, ViewModel, saved state,
environment, theme, or frame-clock locals. A custom host owns those providers and must dispose the
session before abandoning its container. One container must have only one mounted-tree owner.
Disposal is idempotent and terminal: later caller-initiated render or setRenderingActive calls
throw IllegalStateException. A frame callback already queued inside the Android runtime is
cancelled or ignored and cannot render after disposal.
The frame-aligned runtime uses a dedicated internal callback instead of a generic captured
function on the UI-thread dispatch path. Cross-thread requests still post one bounded Runnable;
same-thread requests and Choreographer delivery add no callback wrapper per frame.
AndroidEnvironmentBridge.fromContext(context) maps density, font scale, locales, and layout
direction to UiEnvironmentValues. AndroidOverlayHostDefaults.androidOrNoOp(root) performs an
optional neutral-overlay ServiceLoader lookup without moving Android service discovery into UI
Foundation. Zero providers returns no-op; multiple providers fail because classpath order may not
choose a design system. Standard Activity and Fragment roots use explicit factories instead.
Custom hosts that need Android resources install AndroidResourceEnvironment(context). Inside the
provider, content can call stringResource, formatted strings, pluralStringResource,
colorResource, logical or pixel dimension lookups, boolean/integer lookups, and string/integer
array lookups. LocalAndroidContext.current and LocalAndroidResources.current are bounded escape
hatches for uncommon APIs; access without the provider fails with an installation error.
The provider observes Android configuration callbacks, republishes density, font scale, locales,
direction, and a monotonic resource revision, and unregisters with the mounted composition. Use one
host-scoped AndroidResourceRefreshController after replacing a stable Context wrapper or another
imperative resource mutation that emits no callback. Calls, callbacks, and disposal are main-thread
work. Resource results are synchronous snapshots; do not retain provider-owned Context or Resources
beyond the session.
fun androidResourceEnvironmentSample(
builder: UiTreeBuilder,
context: Context,
titleResource: Int,
) {
builder.AndroidResourceEnvironment(context) {
Text(stringResource(titleResource))
}
}
Optional session-inspection boundary
The Host reads the neutral RenderSessionInspectionTooling port from one process-local in-memory
slot. No provider is the normal production configuration and freezes to a stable no-op at the first
session. A downstream tooling artifact may use the Q3
installRenderSessionInspectionTooling integration hook during Android component initialization,
before any render session starts. Reinstalling the same instance is idempotent; distinct early
providers disable the port, and late installation is ignored. Installation and first access are
synchronized and perform no classpath scan, file I/O, or Android service lookup. The Host contains
no device-locator protocol, Android component, report writer, View-tree listener, or recurring
inspection lifecycle.
Running-device DSL navigation is implemented downstream by the optional viewcompose-preview
artifact. Add it with debugImplementation to enable the feature. When present in a debuggable
process, it may retain bounded source candidates from the first successful Host, navigation
destination, or pager-page frame through the neutral port. RenderSessionInspectionPolicy tracks
lazy-item, overlay, and preview sessions without enabling their composition-time source capture, so
request-driven node inspection can reach the real child owner without adding high-churn stack
capture. The report uses the runtime trace ID, parent ID, and role rather than a second source-only
identity.
The same registration receives a RenderSessionNodeInspection whose session state is weak outside
the render owner. It calls CoreRenderEngine.inspectMountedNodes only after an explicit request;
AndroidCoreRenderEngine then performs the bounded current-tree traversal and returns weak native
targets. No provider means no inspection state or mounted-node assignment. Live visibility,
mounted nodes, and a private response are read only after Android Studio requests them. Scroll,
layout, rendering-active changes, and session disposal do not publish reports, and Host owns no
overlay or IDE protocol. This ownership follows
ADR-0009.
The no-discovery initialization mechanism is fixed by
ADR-0022.
The registration also receives the neutral Q3 RenderSessionTimingInspection control. An explicit
downstream request may start one finite composition/reconciliation/binding capture; Android Host
only maps the synchronous CoreRenderTimingCollector to the Android renderer and owns no protocol,
poller, report, or Studio UI. The engine preserves composition node identity across reconciliation
and binding, while renderer-only nodes receive an opaque capture-local fallback. Without the
optional tooling artifact and request, normal Host rendering performs zero per-node clock reads and
keeps no timing history.
Native View transaction contract
Reusable integrations implement the typed AndroidViewAdapter<V, S> contract. The adapter class
and constructionKey identify constructor-sensitive state, while key continues to identify
logical content:
fun typedAndroidViewAdapterSample(builder: UiTreeBuilder) {
builder.AndroidView(
adapter = NativeLabelAdapter,
state = "Typed native label",
key = "label",
constructionKey = "default-text-appearance",
modifier = Modifier.nativeView(key = "enabled") { view ->
view.isEnabled = true
},
)
}
private object NativeLabelAdapter : AndroidViewAdapter<TextView, String> {
override val reusePolicy: AndroidViewReusePolicy = AndroidViewReusePolicy.Resettable
override fun create(scope: AndroidViewCreateScope): TextView = TextView(scope.context)
override fun update(scope: AndroidViewUpdateScope<TextView>, state: String) {
scope.view.text = state
}
override fun onReset(
scope: AndroidViewResetScope<TextView>,
reason: AndroidViewResetReason,
) {
scope.view.text = null
}
}
create,update, reset, commit, and release run synchronously on the Android main thread. Creation receives the renderer-supplied themedContext; creation, update, reset, and commit scopes also expose the VNode's immutableUiEnvironmentValues.stateremains caller-owned.updateapplies its complete replay-safe configuration and may run again during rollback. Ordinary same-identity updates never invokeonReset.- A changed adapter implementation class or
constructionKeycreates and updates a detached candidate. Failure releases only that candidate and preserves the committed View; success swaps it atomically and releases the displaced View once. AndroidViewReusePolicy.Resettableopts the node into mounted-tree reuse across lazy keys.onReset(..., MountedTreeReuse)runs only after the old logical session, effects, and saveable lease have ended and before the new key's update. The defaultNeverpolicy prevents the containing mounted tree from crossing keys.onCommitruns only after the complete composition transaction commits.onReleaseruns once whenever a created View is permanently abandoned: candidate rollback, committed replacement or removal, non-reusable session disposal, or final reuse-cache eviction.lifecycleModeis bounded diagnostic metadata. Raw adapters reportNone; AndroidX integration adapters reportAdapterManaged. Host records the value but never installs an owner observer or changes transaction ordering because of it.
The callback-based AndroidView(factory, update, ...) overload remains the low-level escape hatch
and delegates to the same typed transaction path. Its trailing constructionKey has the same
replacement semantics, and supplying onReset opts into only cross-key mounted-tree reuse.
Native animation and graphics interop
AndroidAnimationInterop starts platform animators without moving their lifecycle into the
composition animation engine. MotionLayoutView hosts an AndroidX MotionLayout, while
Modifier.androidAnimation applies replay-safe native animation properties during binding:
fun platformAnimationInteropSample(target: View) =
AndroidAnimationInterop.startObjectAnimator(
target,
"alpha",
0f,
1f,
durationMillis = 180L,
)
fun UiTreeBuilder.motionLayoutInteropSample() {
MotionLayoutView(
factory = { context -> MotionLayout(context) },
update = { layout -> layout.progress = 0f },
modifier = Modifier.androidAnimation(key = "settled-alpha") { view ->
view.alpha = 1f
},
)
}
AndroidGraphicsInterop exposes the API-gated platform RenderEffect boundary, and
Modifier.androidGraphics applies native graphics state through the same retained-View binding
path:
fun platformGraphicsInteropSample(target: View): Modifier {
val effect = AndroidGraphicsInterop.createBlurEffect(radiusX = 12f, radiusY = 12f)
AndroidGraphicsInterop.applyRenderEffect(target, effect)
return Modifier.androidGraphics(key = "native-graphics") { view ->
view.alpha = 1f
}
}
Saved state, scheduling, and threading
viewComposeSaveableStateRegistry(owner) binds framework saveable state to an Android
SavedStateRegistryOwner. View creation, reconciliation, explicit rendering, and disposal are
main-thread work. State invalidations coalesce onto the next Choreographer frame, while an explicit
RenderSession.render() remains synchronous until terminal disposal.
The installed AndroidCoreRenderEngine also translates UI Foundation's Q3 observed-property SPI
to exact Android Renderer targets. Property-only frames keep the mounted root list and target map
stable, validate that every target still belongs to the committed frame, and return only commit
effects, failures, and optional diagnostics. A foreign or stale target fails instead of triggering
a whole-tree render.
Related documentation
The generated reference is available in the
viewcompose-host-android API tree.
Compatibility notes
The Activity and Fragment setUiContent extensions moved to viewcompose-android in the hard-cut
five-layer architecture. No compatibility facade remains in this low-level artifact.
Version 0.1.0-alpha04 restricts overlay service discovery to one neutral provider; standard roots
choose their backend explicitly, and duplicate providers are a configuration error.
Device source inspection moved out of this artifact. The alpha renderInto hard cut replaces the
three render callbacks with one RenderDiagnostics configuration and adds typed role/parent
integration inputs. Custom platforms may keep the default null port. Applications that use
Inspect Device Diagnostics retain viewcompose-preview in a debug configuration, while release
builds carry no device-inspector implementation.