Separate state-holder wiring from UI rendering. The state-holder composable talks to ViewModels, components, flows, navigation, and side effects. The UI composable takes plain immutable UI state plus callbacks and describes layout.
This keeps screens previewable, testable, and easier to reuse across Android, Desktop, TV, and KMP/CMP targets.
When to use this skill
Use this when a Compose screen:
Takes a ViewModel, component, controller, navigator, repository, or service directly.
Collects app/business state or side effects in the same function that lays out most UI.
Passes a whole state holder into child composables instead of explicit state and callbacks.
Is hard to preview because it needs dependency injection, navigation, lifecycle, or fake services.
Has UI tests that must construct a full app stack to verify a simple layout branch.
The pattern
Use a small public state-holder composable:
kotlin
@Composablefun ProfileScreen(component: ProfileComponent, modifier: Modifier = Modifier) { val state by component.state.collectAsStateWithLifecycle() ProfileScreen( state = state, onNameChange = component::onNameChange, onSaveClick = component::save, onBackClick = component::back, modifier = modifier, )}
Then put UI in a plain composable that knows nothing about the state holder:
@Composableprivate fun ProfileContent( name: String, isSaving: Boolean, canSave: Boolean, onNameChange: (String) -> Unit, onSaveClick: () -> Unit, onBackClick: () -> Unit, modifier: Modifier = Modifier,) { // Layout only.}
Rules of thumb
Concern
State-holder composable
UI composable
Collect ViewModel/component state
Yes
No
Collect one-shot effects
Yes, or a tiny sibling effect handler
Usually no
Hold dependency-injected objects
Yes
No
Accept immutable UI state
Usually passes it through
Yes
Accept lambdas for user events
Wires them
Calls them
Own layout, modifiers, semantics, test tags
No/minimal
Yes
Own UI-local state like scroll, focus, text input, animation, interaction
Sometimes seeds it
Yes
Preview/screenshot friendly
Not necessarily
Yes
The "no collection in UI composables" rule is about app/business state and side-effect streams. Plain UI composables can still own UI-local framework state: rememberScrollState, rememberLazyListState, FocusRequester, focus state, animation state, TextFieldState, MutableInteractionSource.collectIsPressedAsState(), and similar behavior that belongs to the rendered widget.
If that UI-local state grows into coordinated behavior with multiple related fields and operations, use compose-state-hoisting to decide whether it should become a plain state holder class remembered in composition.
What to pass
Pass the smallest useful UI contract:
Prefer a dedicated UiState/State object over many unrelated primitives when the screen has real state.
Prefer explicit lambdas (onRetryClick, onItemSelected) over passing a whole component.
Keep domain models out of the UI composable if they force business rules into UI. Map to UI models when the UI needs a different shape.
Keep navigation as callbacks. The UI composable says "user clicked back", not "navigate to route X".
Frame-rate or UI-local values that should not force whole-tree recomposition when they change: prefer provider lambdas and deferred reads per compose-state-deferred-reads.