<< All versions

Skill v1.0.0

currentLLM-judged scan95/100
santimattius/performance-compose-skills/compose-views-interop
──Details
PublishedOctober 3, 2026 at 04:50 PM
Content Hashsha256:9549741faa3debdd...
Git SHA
──Files
Files (1 file, 13.2 KB)
SKILL.md13.2 KBactive
SKILL.md · 280 lines · 13.2 KB

version: "1.0.0" name: compose-views-interop description: > (Android-only — uses android.view.View) Performance-first View↔Compose interoperability during incremental migration: ComposeView, AndroidView, ViewCompositionStrategy, RecyclerView/ViewPager2 hybrids, Dialog/BottomSheet, Fragment and multi-Activity hosting. Trigger: AndroidView, ComposeView, View interop, custom View in Compose, RecyclerView Compose, hybrid screen, migration coexistence, ViewCompositionStrategy. license: Apache-2.0 metadata: author: Santiago Mattiauda version: "1.0"


Performance First

Every Compose frame runs three phases. Cost rises left-to-right; restart scope shrinks left-to-right.

PhaseWhat RunsRestart CostTrigger
CompositionComposable functions, state readsHIGH (whole scope)State read in composable body
LayoutMeasure + placeMEDIUM (subtree)State read in measure lambda
DrawingCanvas commands, graphicsLayerLOW (single node)State read in draw/graphicsLayer lambda

Interop boundary costs

BoundaryExtra costPhase impact
AndroidViewReal View measure/layout/draw per frame`update` runs in Composition — any State read there retriggers subtree
ComposeViewSeparate composition owner + View attachWrong ViewCompositionStrategy → leaked compositions or state loss on detach
Hybrid scrollTwo input systemsLayout + jank if nested scroll not coordinated
Mixed APKBuild/size (not runtime by default)See references/interop-performance-checklist.md

Rule: at interop boundaries, minimize Composition-phase work; prefer stable parameters and lifecycle-correct disposal.


Performance Toolchain

WARNING: NEVER profile in debug builds. Debug disables R8, inlining, and Strong Skipping — numbers are meaningless. Always profile a release build with profileable enabled or a benchmark build type.
ToolUse ForWhen
Layout Inspector (recomposition counts)Hot AndroidView / parent recomposing on scrollHybrid screens flagged in audit
Composition TracingFrame spikes at interop nodesWhen Layout Inspector shows boundary churn
MacrobenchmarkScroll/startup on hybrid screensPer-release regression on RecyclerView/ViewPager2 hosts
Baseline ProfilesAOT paths outside Compose library profileApp-specific View-heavy startup
android docs searchValidate interop API claimsBefore recommending strategy changes
Compose Compiler ReportsUnstable params into AndroidView updateWhen update block recomposes every frame

When to Use

Load this skill when:

  • Hosting Compose inside Views (ComposeView, XML, Fragment, Dialog, RecyclerView ViewHolder, ViewPager2 page)
  • Embedding Views inside Compose (AndroidView, AndroidViewBinding, legacy custom Views)
  • Choosing `ViewCompositionStrategy` for Fragment, Dialog, or pooling containers
  • RecyclerView or ViewPager2 mixes Compose and View children
  • Single-Activity + Fragments or multi-Activity legacy navigation with partial Compose
  • Auditing or fixing performance regressions during incremental migration (not greenfield Compose-only apps)

When NOT to Use

User intentLoad instead
Migrate one XML layout end-to-end (10-step workflow)Google's migrate-xml-views-to-jetpack-compose
Nav3 scenes, AndroidFragment, NavDisplaycompose-navigation-nav3 (v1 interop defers Nav3 — see Follow-up)
Unstable params / skipping inside pure Composecompose-composition-core
LaunchedEffect, DisposableEffect, coroutines at boundarycompose-effects
ViewModel scope, UiState, Screen/Content splitcompose-architecture
Accessibility on merged treescompose-quality
Full project recomposition auditcompose-audit → routes INTEROP-* here

v1 exclusions (androidx only)

  • No Accompanist — use androidx ViewPager2, Compose, ui-viewbinding only
  • No Nav3 `AndroidFragment` / scene recipes — follow-up change
  • No CMP desktop View interop

CMP Applicability

Canonical CMP rules: `../_shared/cmp-platform.md`
⚠️ This skill is Android-only. All APIs (AndroidView, ComposeView, ViewCompositionStrategy, RecyclerView, Fragment) use android.view.View. There is no CMP equivalent of this skill.
Source setStatusNotes
commonMain❌No android.view.View in commonMain — the entire skill is out of scope
androidMain✅Full skill content applies
iosMain❌Use UIKitView, UIKitViewController, ComposeUIViewController — outside this skill's scope
desktopMain❌Use SwingPanel / SwingInterop — outside this skill's scope
wasmJsMain❌Use HtmlView (experimental) — outside this skill's scope

Status legend: ✅ fully supported · ⚠️ partial / version-gated · ❌ Android-only.

CMP scope: This skill covers Android View interop ONLY. For iOS embedding (UIKitView, ComposeUIViewController), desktop (SwingPanel), or web (HtmlView), see `../_shared/cmp-platform.md#7-view-interop-across-platforms`.


Interop decision tree

Need UI in a legacy View host (XML, Fragment, RV ViewHolder, Dialog)?
├── YES → ComposeView + setContent
│ ├── Fragment / Dialog? → ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
│ ├── RecyclerView item? → Default or DisposeOnDetachedFromWindowOrReleasedFromPool + stable setContent (see recycler-view-interop)
│ └── Sole Activity window? → Default often OK
└── NO → Need legacy View inside Compose tree?
├── SDK has no Compose API (MapView, WebView, AdView, …)? → AndroidView (factory + lean update)
├── Small legacy XML snippet during migration? → AndroidViewBinding (not full-screen)
├── Custom View / View DS widget? → AndroidView short-term; plan Compose rewrite (custom-view-wrappers)
└── UI rebuildable in Compose? → Pure Compose (do NOT use AndroidView)

Source of truth at boundary: hoist shared state in ViewModel / UDF holder; avoid two owners mutating the same View property from Compose and View code.


Critical patterns

1. AndroidView — factory once, lean update

kotlin
// CORRECT — SDK gap; factory creates View once
AndroidView(
factory = { context -> MapView(context).apply { /* one-time setup */ } },
update = { map ->
// Only sync when camera/bounds actually change — avoid heavy work every recomposition
if (map.tag != targetBounds) {
map.moveCamera(targetBounds)
map.tag = targetBounds
}
},
onRelease = { it.onDestroy() },
modifier = Modifier.fillMaxSize(),
)
// WRONG — remembers View outside AndroidView; update does heavy allocation
val view = remember { MyChartView(context) }
AndroidView(factory = { view }, update = { it.rebuild(chartData) }) // rebuild every recomposition

See references/android-view-in-compose.md.

2. ComposeView — match ViewCompositionStrategy to host

HostStrategyWhy
Fragment onCreateViewDisposeOnViewTreeLifecycleDestroyedAlign with viewLifecycleOwner; avoid state loss on back stack
DialogFragment / BottomSheetDisposeOnViewTreeLifecycleDestroyedWindow lifecycle; see dialog reference
RecyclerView ViewHolderDisposeOnDetachedFromWindowOrReleasedFromPool (default)Pooling-safe disposal
Activity-only ComposeView in XMLDefault often OKDetach from window disposes
kotlin
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
composeView.setContent {
MaterialTheme { ProfileContent() }
}

Anti-pattern: calling setContent { } on every onBindViewHolder — use stable content and pass state via parameters. See references/recycler-view-interop.md.

3. Lazy list AndroidView — enable reuse (Compose 1.4+)

kotlin
AndroidView(
factory = { ctx -> MyView(ctx) },
update = { it.bind(item) },
onReset = { it.clear() }, // required for reuse in LazyColumn/LazyRow
onRelease = { it.dispose() },
)

4. Window insets — one consumer per hierarchy root

  • View-root screen: consume insets in View layer; set ComposeView.consumeWindowInsets = false when appropriate
  • Compose-root screen: consume in Compose; pad AndroidView with Modifier.windowInsetsPadding

See android docs — insets Views + Compose.

5. State across boundaries

  • Compose → View: update block or SideEffect / callback from stable hoisted state
  • View → Compose: listener updates ViewModel / MutableState held above interop node
  • Never: duplicate source of truth on both sides without a single owner

See references/view-binding-bridge.md and compose-architecture for ViewModel scoping.

6. Dialog / BottomSheet hybrids

Use ComposeView with fragment/dialog lifecycle strategy; avoid retaining composition after dismiss. Material Compose Dialog / ModalBottomSheet can coexist during migration — pick one owner per overlay.

See references/dialog-bottomsheet-hybrids.md.

7. Topology guides

TopologyReference
Single Activity + Fragment back stacksingle-activity-fragments.md
Multiple Activitiesmulti-activity-patterns.md
RecyclerView ↔ Composerecycler-view-interop.md
ViewPager2 pagesviewpager2-interop.md

Anti-patterns (quick reference)

Anti-patternPhase / riskFix
AndroidView for Button/Text you can write in ComposeComposition tax foreverMigrate UI to Compose
Heavy work in AndroidView updateComposition every state tickDiff updates; hoist stable ids
setContent in RV onBind every bindCritical jankOne composition per holder; update state
Default strategy in FragmentState loss / leaksDisposeOnViewTreeLifecycleDestroyed
remember { View() } outside AndroidViewStale context / leaksCreate in factory
Two scroll parents without nested connectionLayout janknestedScroll bridge or single scroller
Profile debug hybrid screenFalse conclusionsRelease + Macrobenchmark

Commands

bash
# Validate official interop guidance
android docs search "ComposeView ViewCompositionStrategy"
android docs fetch "kb://android/develop/ui/compose/migrate/interoperability-apis/views-in-compose"
# Audit signals in consumer apps
rg -n 'AndroidView\s*\(' -t kotlin
rg -n 'ComposeView|setViewCompositionStrategy' -t kotlin
rg -n 'setContent\s*\{' -t kotlin --glob '*ViewHolder*'

Related skills

SkillRelationship
migrate-xml-views-to-jetpack-compose (Google)Per-screen migration workflow
compose-composition-coreStability, skipping, deferred reads
compose-architectureViewModel scope at screen root
compose-effectsDisposableEffect for View listeners
compose-auditINTEROP-* patterns route here

Follow-up (out of v1)

Nav3 bridge: replacing Fragment/ViewPager2 Compose hosts with NavDisplay, scene strategies, and dialog scenes — coordinate with compose-navigation-nav3; do not duplicate Nav3 API docs here.


Resources

ReferenceTopic
references/README.mdIndex
android-view-in-compose.mdAndroidView, lazy reuse, SDK-only
compose-view-in-views.mdComposeView, strategies
recycler-view-interop.mdRV ViewHolder, AndroidView in Lazy
viewpager2-interop.mdOff-screen pages, nested scroll
dialog-bottomsheet-hybrids.mdDialog/BottomSheet lifecycle
single-activity-fragments.mdFragment back stack
multi-activity-patterns.mdActivity transitions
view-binding-bridge.mdViewBinding + ComposeView
custom-view-wrappers.mdCustom Views, View DS
interop-performance-checklist.mdVerify hybrid screens

Official: Interoperability APIs, Compare metrics.

All versions