Skip to main content

Migrate Compose Layout, Modifier, and Environment Code

This page compares Jetpack Compose layout, modifier, and composition-local semantics with ViewCompose. It is an engineering migration reference, not an API-name parity table. Similar syntax does not imply equivalent measurement, lifecycle, invalidation, or Android integration behavior.

Baseline, status vocabulary, and verification date

BaselineVersionPurpose
ViewCompose target modulesruntime 0.1.0-alpha02; UI Contract and Host 0.1.0-alpha03; UI Foundation and Renderer 0.1.0-alpha01Target of this migration guide
Compose Runtime, UI, and Foundation1.11.4 stableUpstream semantic reference
Repository Compose dependencies1.7.8Executable comparison baseline in this repository
Repository Kotlin toolchain2.0.21Compilation baseline for comparison code

The upstream baseline is confirmed by the official AndroidX release notes for Compose Runtime, Compose UI, and Compose Foundation. The repository baseline is declared in gradle/libs.versions.toml, lines 3 and 22.

This page uses exactly four capability states:

  • Supported: the migration target protects the relevant observable behavior, although names or implementation details can differ.
  • Partially supported: a practical replacement exists, but an important part of the Compose contract is absent or narrower.
  • Intentionally different: ViewCompose provides a deliberate alternative contract; code must be redesigned rather than renamed.
  • Unsupported: no public equivalent exists in the verified baseline.

Last verified: 2026-08-06.

Re-verification owner: ViewCompose UI Contract, UI Foundation, and Android Renderer maintainers.

Evidence model

The comparison has two evidence layers that must not be conflated:

  1. Official semantic review uses Android Developers API documentation, behavior guides, and AndroidX release notes for Compose 1.11.4. Those sources define the upstream behavior described here.
  2. Local executable evidence uses the independently versioned ViewCompose target set above and repository tests. The repository's Compose 1.7.8 dependency allows compiled comparisons, but it is not used to override a documented Compose 1.11.4 semantic change.

No performance equivalence is claimed. This review did not establish comparable benchmark conditions for Compose layout nodes and Android Views.

Compiled side-by-side starting point

This pair keeps one horizontal layout, one ordered Modifier chain, and one scoped environment value visible on both sides. The snippets are extracted from the compiled :samples:compose-migration module and are checked for exact source agreement by qaQuick.

Compose source:

private val LocalContentPadding = compositionLocalOf { 8.dp }

@Composable
fun ComposeProfileRow(name: String) {
CompositionLocalProvider(LocalContentPadding provides 16.dp) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(LocalContentPadding.current),
) {
BasicText(name)
}
}
}

ViewCompose target:

private val LocalContentPadding = uiLocalOf { 8.dp }

fun UiTreeBuilder.ViewComposeProfileRow(name: String) {
ProvideLocal(LocalContentPadding, 16.dp) {
Row(
verticalAlignment = VerticalAlignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(UiLocals.current(LocalContentPadding)),
) {
Text(name)
}
}
}

The similar shape does not make the engines equivalent. Compose measures layout nodes and tracks CompositionLocal reads; ViewCompose renders Android Views, folds Modifier elements by renderer rules, and treats UiLocal as scoped lookup rather than an invalidation subscription.

Capability matrix

ConceptCompose 1.11.4 behaviorViewCompose verified-set behaviorStatusRequired migration action
Built-in layout containersRow, Column, Box, and Foundation layouts measure Compose layout nodes under Constraints.Row, Column, Box, flow layouts, scrolling containers, and ConstraintLayout emit VNodes that become Android ViewGroup implementations.Partially supportedRecheck defaults, overflow, clipping, weight, and intrinsic-size assumptions on the native View implementation.
Custom measurementLayout, MeasurePolicy, and layout modifier nodes let application code measure and place Compose children. Ordinary measurement permits each child to be measured once.No public general-purpose measure policy, measurable/placeable contract, or layout modifier was found. Custom multi-child measurement requires a renderer extension or an Android ViewGroup hosted through interop.UnsupportedRedesign custom Compose layouts around a built-in container or a lifecycle-owned Android View implementation.
Size and fillLayout modifiers transform or constrain a chain; size remains subject to incoming constraints, and fill APIs can accept fractions where defined.Exact dp dimensions become pixel LayoutParams; fill helpers become MATCH_PARENT. maxWidth, maxHeight, and aspectRatio share one renderer measurement boundary around the complete node.Partially supportedReplace the final measured contract, not just the function name. Audit exact/min/max conflicts, ratio axis preference, fractional fill, required size, and intrinsic sizing.
Padding and marginEach layout modifier participates at its position in the modifier chain. Compose normally represents outside space with layout structure or padding rather than a margin property.Padding is native View content padding. Margin is explicit native parent LayoutParams data. Repeated padding or margin elements resolve to the last element of that type.Intentionally differentFlatten repeated padding and decide explicitly whether former outer padding belongs in parent structure, View padding, or ViewCompose margin.
Scoped parent dataScope-safe modifiers such as RowScope.weight, ColumnScope.weight, alignment, and BoxScope.matchParentSize provide data to a compatible direct parent.RowScope and ColumnScope expose weight and cross-axis alignment; BoxScope exposes alignment. Invalid parent-data use is diagnosed with a warning. There is no verified matchParentSize equivalent.Partially supportedKeep scoped modifiers on direct children of the matching container. Redesign matchParentSize; do not replace it blindly with fillMaxSize.
Constraint parent dataCompose ConstraintLayout consumes layout IDs and constraint parent data inside its own measurement model.The optional ConstraintLayout module maps layout IDs and constraint specs to AndroidX ConstraintLayout LayoutParams and ConstraintSet operations.Partially supportedRevalidate dimensions, baselines, RTL anchors, and dependency cycles against the AndroidX View implementation.
Layout-coordinate animationCompose lookahead-based bounds motion participates in Compose layout coordinates and can coordinate broader visual transitions.Modifier.animateBounds animates one real position-and-size rectangle in the immediate ViewCompose parent's physical pixels. Reparenting, shared elements, and cross-owner visual continuity are not part of this API.Partially supportedKeep the node under one stable parent, migrate the final size/alignment/constraint state, reject simultaneous animateContentSize, and use ordinary destination enter behavior when ownership changes.
Modifier orderModifier elements form an ordered wrapping chain; order can change measurement, drawing, input, focus, and semantics.The source chain is ordered, but the renderer folds it into phase-specific values. Many repeated values are last-wins, some conflicts use fixed precedence, z-index values add, and draw or shadow groups retain order.Intentionally differentClassify every non-trivial chain by its ViewCompose resolution rule before migrating it.
Modifier equality and updateA ModifierNodeElement uses equality to decide whether an existing Modifier.Node is updated.Modifier chains compare their ordered element sequences structurally. Renderer diffing can skip a subtree for equal chains. NativeViewElement equality uses only its stable key and ignores callback identity.SupportedGive native configuration a key that changes when its semantic configuration changes, and do not rely on a fresh lambda instance to force an update.
Custom Modifier.Node lifecyclePublic node APIs provide create/update, attach/detach, invalidation, local reads, and specialized layout, draw, input, or semantics node interfaces.ModifierElement is a renderer marker, not an application lifecycle node. Built-in elements are interpreted by known renderer branches. No public equivalent of custom node attach/detach or capability interfaces exists.UnsupportedUse a supported modifier, replay-safe nativeView, transaction-aware AndroidView, or a reviewed renderer feature. Do not publish an unrecognized element from application code.
Density and font scaleLocalDensity provides dp/sp conversion to layout and drawing code.UiDensity is captured into each VNode environment. Android hosts read density and font scale from resources; renderers convert units at the native boundary.SupportedKeep logical dp/sp in declarations and avoid retaining converted pixels across a new environment snapshot.
Layout direction and localesComposition locals provide layout direction and locale data; logical start/end APIs resolve from that environment.Direction and locale lists are captured in the VNode. The renderer applies native View direction and TextView locales. General edge families provide explicit physical and relative forms, and delayed sessions carry the environment revision.SupportedSelect the relative form for logical start/end intent, retain physical APIs only for deliberate left/right behavior, and run real RTL layout checks.
Composition-local propagationcompositionLocalOf tracks read sites; changing a provided value invalidates the readers. staticCompositionLocalOf invalidates the provider content as a broader unit.UiLocal uses a thread-scoped map while a tree is built. Emits compare a complete local snapshot as an input, but reading UiLocals.current does not itself register an invalidation dependency.Intentionally differentBack changing local values with ViewCompose state or another host invalidation source. Treat local reads as scoped value lookup, not observation.
Delayed content localsLazy and other subcomposed content observes locals through the Compose composition that owns it.Lazy, pager, tab, overlay, and navigation sessions explicitly capture opaque local snapshots and restore them when delayed content renders. Snapshot changes participate in content tokens or session updates.SupportedPreserve stable item/page keys and content tokens, and let the container refresh its captured snapshot rather than retaining a builder.
System bars and IME insetsInsets padding is layout-aware, participates in automatic nested consumption, avoids reapplying an already consumed portion, and follows IME updates and animations.System-bar and IME modifiers install an AndroidX listener on the target View and add selected physical or direction-resolved sides to base padding. Nested ViewCompose modifiers do not exchange consumed-inset state; system-bar and IME values on one View are summed.Partially supportedAssign inset ownership to a deliberate level, avoid duplicate ancestor/descendant application, and avoid combining adjustResize with redundant IME padding.
Android output and View interopCompose normally renders Compose nodes; AndroidView embeds a platform View with factory/update and optional reuse/release callbacks.Every first-party node ultimately becomes an Android View. ViewCompose AndroidView adds transactional rollback and post-transaction commit semantics; nativeView applies replay-safe configuration to the mounted View.Intentionally differentSeparate repeatable configuration from one-shot work and cleanup. Put them in update/native configuration, onCommit, and onRelease respectively.

Two layout engines: Compose constraints and Android Views

Compose layout is a node protocol. A parent passes constraints, children report measured sizes, and the parent places the resulting placeables. The official custom-layout documentation also defines the ordinary single-measure rule and the public Layout escape hatch.

ViewCompose builds immutable VNodes first. The Android renderer then creates native widgets and containers. For example, Text becomes TextView, Row and Column become an oriented DeclarativeLinearLayout, and Box becomes DeclarativeBoxLayout. The definitive mapping is in ViewNodeFactory.kt, lines 55–126. Row and Column retain Android LinearLayout measurement and implement declarative arrangement during native placement; see DeclarativeLinearLayout.kt, lines 21–92.

Consequently, a Compose custom Layout cannot be translated as a normal ViewCompose component. Choose one of these boundaries:

  • express the result with a built-in ViewCompose container;
  • use the optional ConstraintLayout module when constraint parent data is sufficient;
  • host a custom Android ViewGroup through AndroidView when application-specific measurement is essential; or
  • propose a documented renderer feature when the behavior is a reusable framework contract.

The last two choices are not equivalent to receiving Compose Measurable values. Android measure specs, LayoutParams, request-layout propagation, and platform view state remain authoritative.

Size, padding, margin, and fill semantics

Compose chains layout modifiers as ordered participants in constraint propagation. The official constraints and modifier-order guide is the upstream reference for that model.

ViewCompose resolves dimensions through native LayoutParams. The parent-aware precedence is:

  1. a ConstraintLayout dimension, when present;
  2. an axis-specific width or height modifier;
  3. the corresponding axis from size;
  4. the renderer's node-and-parent default.

This precedence is implemented in ViewLayoutParamsFactory.kt, lines 73–99. Exact dp dimensions are converted with the VNode's captured density. Fill helpers map to Android MATCH_PARENT; they do not preserve every fractional or intrinsic Compose option.

Portable maximum bounds and ratios are the exception to direct LayoutParams mapping. The renderer folds maxWidth, maxHeight, and aspectRatio into one synthetic measurement host around the complete modified node. Positive finite values are required. An exact or minimum size that exceeds the declared maximum fails deterministically instead of silently selecting one source; ratio selection uses width first unless matchHeightConstraintsFirst is set. Android's incoming EXACTLY constraint remains authoritative when no size can satisfy both axes and the ratio. This is not a public custom measurement API and does not make arbitrary Compose LayoutModifier code portable.

Edge modifiers have distinct native destinations:

  • padding becomes physical content padding; paddingRelative maps logical start/end before the renderer writes the mounted View;
  • margin supplies physical LayoutParams margins; marginRelative maps logical start/end before the parent LayoutParams are created;
  • offset is a physical View translation; positive offsetRelative.horizontal moves toward logical end and neither form changes sibling measurement or placement; and
  • minimum width and height become View minimum dimensions.

Repeated padding does not create nested layout layers. Physical and relative declarations share one resolved slot per family, so the later padding, margin, or offset declaration replaces the earlier complete value even when their forms differ. Migration should therefore normalize a Compose chain before translating it and preserve intended outer and inner boundaries in the container structure.

The public dimension and edge contracts are in ModifierLayoutExtensions.kt, lines 6–187 and 189–290. Their native LayoutParams application is in ViewLayoutParamsFactory.kt, lines 91–149 and 168–192.

Row, Column, Box, and scoped parent data

Both frameworks use receiver scopes to keep common parent data near a compatible parent. The Compose behavior and matchParentSize distinction are documented in Compose modifiers.

ViewCompose exposes these supported scoped operations:

ScopeSupported parent dataNative destination
RowScopepositive weight; vertical alignhorizontal LinearLayout weight and child gravity
ColumnScopepositive weight; horizontal alignvertical LinearLayout weight and child gravity
BoxScopebox alignFrameLayout child gravity

The declarations and positive-weight check are in LayoutScopes.kt, lines 12–96. Parent-data validation is in ModifierParentDataValidator.kt, lines 28–97.

Scope availability is the supported application API, but it is not a complete runtime type-safety barrier. Contract element classes remain visible to renderer integrations, and an incompatible parent produces a deduplicated warning rather than a render failure. Treat every scoped modifier as direct-child data.

Compose BoxScope.matchParentSize is deliberately called out as unsupported. Compose uses it to match the Box's final size without making that child determine the Box size. ViewCompose fillMaxSize maps to MATCH_PARENT and must not be documented as an equivalent replacement.

ConstraintLayout parent data

The optional ConstraintLayout module supplies layout IDs and constraint item specifications as parent data. The Android renderer consumes those values through AndroidX ConstraintLayout. Fixed dimensions are converted from the child's captured environment; MatchConstraints becomes the Android ConstraintLayout zero-dimension convention.

Both libraries use a dedicated marked content scope, but ViewCompose deliberately keeps the XML-familiar startToStart, topToBottom, and related functions instead of copying Compose anchor objects. ViewCompose separates horizontal, vertical, and baseline target capabilities: using a top/bottom Guideline as a start/end target, or a start/end Guideline as a top/bottom target, fails Kotlin compilation. Nested structural DSLs hide the outer ConstraintLayout receiver, and helper metadata is frozen after content completes rather than collected through ambient thread-local state.

Logical start/end semantics remain environment-driven after mounting. The Android renderer keeps retained helper layoutDirection synchronized with the ConstraintLayout container, so an in-place LTR/RTL change mirrors logical Guidelines and Barriers without replacing their stable identity.

Reusable sets also keep declaration identity typed. Create a reference, pass it to constrain(ref), and use that same reference for links; the removed constrain(ref.id) form cannot drift back to an unrelated string. Modifier.constrain(id, ...) remains an explicit inline XML-migration shortcut. Dimension migration targets the mutually exclusive WrapContent, ConstrainedWrapContent, Fixed, and MatchConstraints algebra rather than independent min/max/percent fields or MatchParent.

The post-release line adds typed chain endpoints and margins, four parent-wrap contribution modes, logical and physical horizontal anchors/Guidelines/Barriers, typed weighted Grid spans/skips, and a declarative CircularFlow that compiles to ordinary circle constraints. It deliberately does not expose AndroidX Grid string grammar, process-global CircularFlow defaults, imperative helper mutation, raw optimization bitmasks, Compose linkTo, or anonymous references. These omissions preserve one typed graph owner and the XML-friendly migration family rather than representing unfinished aliases.

The contract elements are defined in ModifierElementsLayout.kt, lines 117–150. Parent-aware conversion is implemented in ViewLayoutParamsFactory.kt, lines 91–98 and 247–255.

This is a practical migration path, not proof of Compose ConstraintLayout parity. Recheck ConstraintSet merging, baseline connections, logical start/end anchors, circular dependencies, helper capabilities, and dimension defaults against the ViewCompose module contract. The completed revision-6 released/candidate/direct matrix establishes no material change for release safety, not performance leadership: Direct AndroidX remains faster at P95 in all twelve Candidate actions, and five longitudinal actions remain inconclusive. Choose this module for its typed declarative contract, native solver behavior, and transactional safety, not because migration is expected to make every frame faster. See ViewCompose Performance.

Modifier ordering, folding, and equality

Both Modifier types are immutable ordered chains. ViewCompose appends elements without mutating the receiver and compares the resulting sequence structurally; see Modifier.kt, lines 3–56.

The important difference is execution. Compose layout and behavior nodes retain their positions in a wrapping node chain. ViewCompose folds elements into a ResolvedModifiers snapshot consumed by separate renderer phases. The verified folding rules include:

Modifier relationshipViewCompose rule
Repeated scalar or single-slot elementsThe later element of the same type usually replaces the earlier value.
shape and legacy cornerRadiusThey are mutually exclusive; the later one in the chain clears the earlier one.
Repeated zIndexValues are added.
Draw and advanced-shadow groupsGroups retain declaration order.
Axis width/height and sizeAxis-specific values win through fixed LayoutParams precedence, regardless of cross-type chain order.
graphicsLayer and simple alpha, offset, or clipThe graphics-layer value has fixed renderer precedence when supplied.
Physical and relative padding, margin, or offsetThe later declaration replaces the earlier complete value in that family.
System-bar and IME inset paddingThe later physical or relative declaration wins within each inset type; system-bar and IME contributions are then added.

The fold is implemented in ResolvedModifiers.kt, lines 72–172. Do not infer a rule for one modifier family from another family.

Equality also drives reuse. NodeBindingDiffer can skip a subtree when the node, environment, specification, children, and modifier inputs remain equivalent. An environment or modifier change causes a rebind; see NodeBindingDiffer.kt, lines 22–75.

NativeViewElement is a special case. Its equality and hash code use only stableKey, deliberately ignoring callback identity. The contract is in ModifierElementsInteraction.kt, lines 220–249. A new lambda with the same key is not an update signal. Change the key when the semantic native operation changes, or make another observable node input invalidate the binding.

Why Modifier.Node does not migrate directly

Compose recommends Modifier.Node for custom modifier behavior. Its public model includes an immutable element, a retained node, create/update, attach/detach, automatic or explicit invalidation, CompositionLocal access, and specialized node interfaces. The upstream references are Create custom modifiers and the Modifier.Node API.

ViewCompose has no equivalent public lifecycle-node protocol. ModifierElement is a marker for contracts understood by a renderer; see Modifier.kt, lines 59–65. Application-defined implementations are not discovered as custom behavior.

Use these alternatives according to ownership:

  • a supported ViewCompose modifier for framework-defined behavior;
  • nativeView for repeatable configuration of the already mounted View;
  • AndroidView when application code owns a native View and its release lifecycle; or
  • a documented UI-contract and renderer change for a new reusable modifier capability.

An unrecognized ModifierElement is not a safe extension point. nativeView is also not a generic node lifecycle: it has no attach/detach callback and its configuration can be replayed during rollback.

Density, locales, and layout direction

Compose exposes density and logical direction through platform CompositionLocals. The official Compose platform-local reference defines LocalDensity, LocalLayoutDirection, and locale-related locals.

ViewCompose captures an immutable UiEnvironmentValues on every emitted VNode. It contains:

  • UiDensity, including density and font scale;
  • an ordered UiLocaleList;
  • UiLayoutDirection; and
  • a host-owned resourceRevision used to rebind equal Android resource IDs after configuration or imperative resource changes.

The snapshot contract requires a new tree after a platform configuration change. Standard Android hosts schedule that tree automatically through their resource environment; custom hosts must publish a new environment explicitly. See UiEnvironmentValues.kt, lines 92–112. The Android bridge reads resources and configuration in AndroidEnvironmentBridge.kt, lines 15–29. Unit conversion is defined by UiUnits.kt, lines 157–223.

At bind time, the renderer stores the environment on the View, applies native layout direction, and sets TextView locales. That boundary is in ViewModifierApplier.kt, lines 41–55. A changed environment forces a full node rebind rather than a visual-only patch.

Direction support is explicit at the modifier boundary. Existing padding, margin, offset, systemBarsInsetsPadding, and imeInsetsPadding APIs remain physical. Their Relative counterparts resolve logical start/end from the captured UiLayoutDirection on every bind and environment rebind. Positive relative horizontal offset moves toward end: right in LTR and left in RTL. Top, bottom, and vertical offset remain physical.

Every migration involving asymmetric horizontal space still needs an explicit choice. Map Compose start/end intent to a relative API; use the original API only when the product requirement truly means physical left/right. Do not pre-swap values in application code because runtime direction changes and delayed lazy or pager sessions are framework-owned invalidation inputs.

UiLocal versus CompositionLocal

Compose distinguishes tracked compositionLocalOf reads from broad staticCompositionLocalOf invalidation. The upstream behavior is documented in Locally scoped data with CompositionLocal.

ViewCompose UiLocal is a typed handle into a thread-scoped immutable map used while a VNode tree is built. ProvideLocal installs a value for a nested block and restores the prior map afterward. ProvideLocals performs the same operation for multiple bindings. Binding presence is distinct from nullability: an explicitly provided null for a nullable Local overrides a non-null default and survives capture, restore, and delayed child-session propagation. The implementation is in UiLocals.kt, lines 3–103, and LocalValue.kt, lines 35–120.

The crucial migration rule is that UiLocals.current(local) is lookup, not observation. It does not register that call site as a dependent reader. Instead, UiTreeBuilder.emit captures the complete current local snapshot as one of its composition inputs. When another invalidation already causes composition and that snapshot differs, the node group is rebuilt. See UiTreeBuilder.kt, lines 66–124 and 192–214.

Therefore:

  • store changing source data in ViewCompose state, not only in a plain provided object;
  • do not expect changing a mutable field inside an equal local value to schedule rendering;
  • prefer immutable local values with meaningful equality;
  • remember that a changed local snapshot can invalidate more work than a tracked Compose local reader; and
  • custom hosts must serialize tree building on the owning renderer thread.

Delayed content and local snapshots

Lazy collections, pagers, tabs, overlays, and navigation can render content after its declaration scope returns. ViewCompose preserves locals explicitly for those boundaries.

For lazy lists, LazyItemCollector captures a LocalSnapshot, includes it in the effective content token, creates a child session with that snapshot, and refreshes both the snapshot and content closure on update. See LazyCollectionScope.kt, lines 147–193, and WidgetLazyListItemSession.kt, lines 8–72.

This preserves nested local values across holder reuse, but it does not remove the caller's identity responsibilities. Keys must remain stable and unique. A content token must change when captured business values outside the local snapshot change. Do not retain or invoke a UiTreeBuilder after its content block returns.

System bar and IME insets

Compose inset padding applies current inset values during layout and communicates consumed portions to nested modifiers. The official insets UI guide explains nested consumption, size modifiers, and IME animation behavior.

ViewCompose offers physical and relative forms for each supported inset type:

  • systemBarsInsetsPadding, which selects physical system-bar sides; and
  • systemBarsInsetsPaddingRelative, which selects logical start/end system-bar sides;
  • imeInsetsPadding, which defaults to the physical bottom side; and
  • imeInsetsPaddingRelative, which defaults to the same bottom side and can select logical start/end sides.

The renderer installs an AndroidX WindowInsetsCompat listener, records base padding, and adds the selected inset pixels. A direction change re-resolves relative selectors from the node environment; the renderer uses current root insets when available, otherwise clears the obsolete physical contribution until the platform dispatches the replacement. Removing both modifiers restores base padding and removes the listener. The implementation is in ModifierInsetsApplier.kt, lines 11–128.

Unlike Compose, the listener returns the incoming insets unchanged and does not communicate how much an ancestor ViewCompose modifier applied. Nested ViewCompose inset-padding modifiers can therefore add the same inset again. System-bar and IME padding selected on the same View are also summed, rather than reduced by a shared consumption model.

Migration rules:

  1. Choose one owner for each inset edge whenever possible.
  2. Inspect native ancestors and embedded Views for their own inset handling.
  3. Do not combine Activity adjustResize behavior with redundant imeInsetsPadding unless the resulting displacement is intentionally verified.
  4. Test gesture navigation, three-button navigation, landscape, RTL, display cutouts, and an IME transition on a real hosted screen.
  5. Do not claim Compose nested-consumption or same-frame layout parity.

Unit tests protect defaults, physical/relative precedence, direction re-resolution, and compatible WindowInsets dispatch. Device-level animation, mixed-tree consumption, and platform-version dispatch behavior remain certification boundaries recorded below.

Android View output and interop

Compose normally renders its own UI nodes and uses AndroidView as an interop boundary. The official Views in Compose guide defines factory, update, reuse, reset, and release behavior.

ViewCompose is different at the root: every first-party VNode becomes an Android View. Its AndroidView API is still a distinct ownership boundary for an application-created View and has a transaction-aware lifecycle:

CallbackViewCompose contract
factoryRuns only when reconciliation needs a new native node.
updateRepeatable configuration during insertion, patching, or rollback.
onResetOptional repeatable reset before a retained View is rebound.
onCommitOne-shot work published only after the complete View-tree transaction commits.
onReleaseOne-shot cleanup whenever a created View is permanently abandoned, including committed removal, session disposal, or rollback of an uncommitted candidate.

The public contract is in AndroidInteropDsl.kt, lines 11–82. Mounting and commit scheduling are in ViewTreePatchPipeline.kt, lines 527–579.

update, onReset, and nativeView must not start non-repeatable external work. A failed frame can restore the previously committed native tree and replay configuration. Use onCommit for operations that must happen only after success and onRelease for owned-resource cleanup. The public contract and renderer tests both include rollback of an uncommitted candidate as permanent abandonment.

Migration checklist

  1. Record the source Compose version and the exact ViewCompose module versions being targeted.
  2. Classify each layout as built-in, constraint-based, or custom-measured.
  3. Replace layout behavior before translating visual modifier names.
  4. Normalize repeated size, padding, margin, graphics-layer, and draw elements according to the ViewCompose folding rules.
  5. Replace compatible maximum-size and ratio chains with maxWidth, maxHeight, and aspectRatio, then test exact/minimum conflicts and bounded versus unbounded parents.
  6. Keep parent-data modifiers on direct children of the matching scope and redesign matchParentSize uses.
  7. Map logical start/end intent to relative modifiers; keep physical APIs only for deliberate left/right behavior.
  8. Move changing provided values behind ViewCompose state; do not rely on UiLocal read tracking.
  9. Assign system-bar and IME inset ownership explicitly across View and ViewCompose boundaries.
  10. Separate Android View replay-safe configuration, post-commit work, and release cleanup.
  11. Add behavior tests for measurement, RTL, local updates, delayed sessions, inset dispatch, and interop rollback before declaring the migration complete.

Source and executable evidence

The following local evidence protects the claims in this page:

Compiled API samples cover Modifier chain construction, relative layout edges, and AndroidView interop, but no compiled migration sample demonstrates a Compose custom layout replacement or real nested WindowInsets behavior. This page intentionally avoids embedding a second, non-compiled source of truth.

Known gaps and re-verification triggers

The following gaps remain part of the migration contract:

  • no public custom measurement or Modifier.Node equivalent;
  • no verified BoxScope.matchParentSize equivalent;
  • no tracked-versus-static UiLocal variants;
  • no nested inset-consumption protocol;
  • no end-to-end WindowInsets animation or mixed View/ViewCompose consumption test; and
  • no device matrix yet certifies relative inset selection across every supported Android version.

The owner must re-verify this page when any of these events occurs:

  1. Compose Runtime, UI, or Foundation advances the selected semantic baseline.
  2. The repository Compose or Kotlin executable baseline changes.
  3. A public layout, parent-data, modifier, environment, local, inset, or AndroidView contract changes.
  4. The renderer changes modifier folding, LayoutParams precedence, environment rebinding, or native transaction behavior.
  5. A new compiled migration sample or instrumentation test closes one of the recorded gaps.

Re-verification must review official upstream documentation first, then the current ViewCompose source and tests. Passing a repository build against an older Compose artifact is not sufficient evidence that a newer upstream semantic contract is unchanged.