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
| Baseline | Version | Purpose |
|---|---|---|
| ViewCompose target modules | runtime 0.1.0-alpha02; UI Contract and Host 0.1.0-alpha03; UI Foundation and Renderer 0.1.0-alpha01 | Target of this migration guide |
| Compose Runtime, UI, and Foundation | 1.11.4 stable | Upstream semantic reference |
| Repository Compose dependencies | 1.7.8 | Executable comparison baseline in this repository |
| Repository Kotlin toolchain | 2.0.21 | Compilation 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:
- 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.
- 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
| Concept | Compose 1.11.4 behavior | ViewCompose verified-set behavior | Status | Required migration action |
|---|---|---|---|---|
| Built-in layout containers | Row, 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 supported | Recheck defaults, overflow, clipping, weight, and intrinsic-size assumptions on the native View implementation. |
| Custom measurement | Layout, 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. | Unsupported | Redesign custom Compose layouts around a built-in container or a lifecycle-owned Android View implementation. |
| Size and fill | Layout 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 supported | Replace 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 margin | Each 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 different | Flatten repeated padding and decide explicitly whether former outer padding belongs in parent structure, View padding, or ViewCompose margin. |
| Scoped parent data | Scope-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 supported | Keep scoped modifiers on direct children of the matching container. Redesign matchParentSize; do not replace it blindly with fillMaxSize. |
| Constraint parent data | Compose 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 supported | Revalidate dimensions, baselines, RTL anchors, and dependency cycles against the AndroidX View implementation. |
| Layout-coordinate animation | Compose 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 supported | Keep 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 order | Modifier 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 different | Classify every non-trivial chain by its ViewCompose resolution rule before migrating it. |
| Modifier equality and update | A 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. | Supported | Give 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 lifecycle | Public 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. | Unsupported | Use 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 scale | LocalDensity 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. | Supported | Keep logical dp/sp in declarations and avoid retaining converted pixels across a new environment snapshot. |
| Layout direction and locales | Composition 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. | Supported | Select 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 propagation | compositionLocalOf 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 different | Back changing local values with ViewCompose state or another host invalidation source. Treat local reads as scoped value lookup, not observation. |
| Delayed content locals | Lazy 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. | Supported | Preserve stable item/page keys and content tokens, and let the container refresh its captured snapshot rather than retaining a builder. |
| System bars and IME insets | Insets 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 supported | Assign inset ownership to a deliberate level, avoid duplicate ancestor/descendant application, and avoid combining adjustResize with redundant IME padding. |
| Android output and View interop | Compose 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 different | Separate 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
ViewGroupthroughAndroidViewwhen 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:
- a ConstraintLayout dimension, when present;
- an axis-specific
widthorheightmodifier; - the corresponding axis from
size; - 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:
paddingbecomes physical content padding;paddingRelativemaps logical start/end before the renderer writes the mounted View;marginsupplies physical LayoutParams margins;marginRelativemaps logical start/end before the parent LayoutParams are created;offsetis a physical View translation; positiveoffsetRelative.horizontalmoves 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:
| Scope | Supported parent data | Native destination |
|---|---|---|
RowScope | positive weight; vertical align | horizontal LinearLayout weight and child gravity |
ColumnScope | positive weight; horizontal align | vertical LinearLayout weight and child gravity |
BoxScope | box align | FrameLayout 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 relationship | ViewCompose rule |
|---|---|
| Repeated scalar or single-slot elements | The later element of the same type usually replaces the earlier value. |
shape and legacy cornerRadius | They are mutually exclusive; the later one in the chain clears the earlier one. |
Repeated zIndex | Values are added. |
| Draw and advanced-shadow groups | Groups retain declaration order. |
Axis width/height and size | Axis-specific values win through fixed LayoutParams precedence, regardless of cross-type chain order. |
graphicsLayer and simple alpha, offset, or clip | The graphics-layer value has fixed renderer precedence when supplied. |
| Physical and relative padding, margin, or offset | The later declaration replaces the earlier complete value in that family. |
| System-bar and IME inset padding | The 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;
nativeViewfor repeatable configuration of the already mounted View;AndroidViewwhen 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
resourceRevisionused 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; andsystemBarsInsetsPaddingRelative, which selects logical start/end system-bar sides;imeInsetsPadding, which defaults to the physical bottom side; andimeInsetsPaddingRelative, 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:
- Choose one owner for each inset edge whenever possible.
- Inspect native ancestors and embedded Views for their own inset handling.
- Do not combine Activity
adjustResizebehavior with redundantimeInsetsPaddingunless the resulting displacement is intentionally verified. - Test gesture navigation, three-button navigation, landscape, RTL, display cutouts, and an IME transition on a real hosted screen.
- 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:
| Callback | ViewCompose contract |
|---|---|
factory | Runs only when reconciliation needs a new native node. |
update | Repeatable configuration during insertion, patching, or rollback. |
onReset | Optional repeatable reset before a retained View is rebound. |
onCommit | One-shot work published only after the complete View-tree transaction commits. |
onRelease | One-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
- Record the source Compose version and the exact ViewCompose module versions being targeted.
- Classify each layout as built-in, constraint-based, or custom-measured.
- Replace layout behavior before translating visual modifier names.
- Normalize repeated size, padding, margin, graphics-layer, and draw elements according to the ViewCompose folding rules.
- Replace compatible maximum-size and ratio chains with
maxWidth,maxHeight, andaspectRatio, then test exact/minimum conflicts and bounded versus unbounded parents. - Keep parent-data modifiers on direct children of the matching scope and redesign
matchParentSizeuses. - Map logical start/end intent to relative modifiers; keep physical APIs only for deliberate left/right behavior.
- Move changing provided values behind ViewCompose state; do not rely on
UiLocalread tracking. - Assign system-bar and IME inset ownership explicitly across View and ViewCompose boundaries.
- Separate Android View replay-safe configuration, post-commit work, and release cleanup.
- 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:
- Modifier immutability, structural equality, declaration order, draw order, and parent-data
construction:
ModifierContractTest.kt, including the five public relative modifier contracts. Their Q3 compiled usage is inUiContractNodeSamples.kt. - Maximum and aspect-ratio contract validation and compiled use:
NativeWidgetContractValidationTest.ktandlayoutConstraintModifierSample; Android measurement is covered byLayoutConstraintHostTestandLayoutConstraintNodeWrapperTestin the renderer module. - Modifier folding, additive z-index, ordered shadows, and ConstraintLayout parent data:
ResolvedModifiersTest.kt, including last-declaration-wins across physical and relative edge forms. - Compatible and incompatible scoped parent data:
ModifierParentDataValidatorTest.kt, lines 31–159. - Structural modifier equality and environment-driven renderer rebind:
NodeBindingDifferTest.kt, lines 115–141. - Density, locales, direction, nested environment values, and capture into a VNode:
EnvironmentTest.kt, lines 15–68. - Density-sensitive ConstraintLayout resolution:
DeclarativeConstraintLayoutEnvironmentTest.kt, lines 21–79. - Nested
UiLocalprovision, restoration, and explicit snapshot restoration:BusinessLocalApiTest.kt, lines 13–103. - Local-snapshot stability and environment-driven subtree replacement:
SubtreeRecompositionTest.kt, lines 59–123. - Delayed lazy, pager, and tab content tokens changing with captured locals:
LazyContentLocalPropagationTest.kt, including direction-driven environment revisions for delayed lazy and pager sessions. - Runtime direction changes, physical padding/margin/offset results, and inset selector rebinding:
ViewTreeRenderTransactionTest.kt. - Relative margins under native ConstraintLayout-compatible LayoutParams:
ViewLayoutParamsFactoryRelativeTest.kt. - Insets modifier defaults and coexistence:
InsetsPaddingModifierTest.kt, while the transaction test above covers compatible dispatch. Neither proves device animation or nested consumption. - Native modifier stable-key equality:
NativeViewElementTest.kt, lines 14–55. - AndroidView rollback, commit publication, and release failure isolation:
ViewTreeRenderTransactionTest.kt, lines 330–341 and 393–470.
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.Nodeequivalent; - no verified
BoxScope.matchParentSizeequivalent; - no tracked-versus-static
UiLocalvariants; - 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:
- Compose Runtime, UI, or Foundation advances the selected semantic baseline.
- The repository Compose or Kotlin executable baseline changes.
- A public layout, parent-data, modifier, environment, local, inset, or AndroidView contract changes.
- The renderer changes modifier folding, LayoutParams precedence, environment rebinding, or native transaction behavior.
- 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.