Loading...
Loading...
Use when deciding where Jetpack Compose UI element state or UI logic should live: local remember state, hoisted composable parameters, a plain state holder class, or a screen-level ViewModel/component.
npx skill4agent add chrisbanes/skills compose-state-hoisting| Situation | Owner |
|---|---|
| One composable reads/writes simple state | Keep local with |
| Sibling or parent composables need to read/write it | Hoist state and events to their lowest common composable ancestor |
| Related UI element state plus UI logic is making a composable hard to read, preview, or test | Extract a plain state holder class remembered in composition |
| Repository calls, persistence, business rules, or screen UI state production are involved | Use a screen-level state holder such as a |
rememberclearsubmitjumpToTopopenFiltersremember...State@Stable
class ProductSearchState(
query: String,
private val listState: LazyListState,
private val focusRequester: FocusRequester,
) {
var query by mutableStateOf(query)
private set
var filtersOpen by mutableStateOf(false)
private set
val canClear: Boolean
get() = query.isNotEmpty()
fun updateQuery(value: String) {
query = value
}
fun clear() {
query = ""
focusRequester.requestFocus()
}
suspend fun jumpToTop() {
listState.animateScrollToItem(0)
}
}
@Composable
fun rememberProductSearchState(
initialQuery: String = "",
listState: LazyListState = rememberLazyListState(),
focusRequester: FocusRequester = remember { FocusRequester() },
): ProductSearchState {
return remember(listState, focusRequester) {
ProductSearchState(initialQuery, listState, focusRequester)
}
}@Composable
fun ProductSearchPanel(
state: ProductSearchState = rememberProductSearchState(),
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
SearchField(
query = state.query,
onQueryChange = state::updateQuery,
onClear = state::clear,
)
JumpToTopButton(onClick = {
scope.launch { state.jumpToTop() }
})
}rememberLazyListStateFocusRequesterPagerStateDrawerStateTextFieldStaterememberCoroutineScopeLaunchedEffectviewModelScoperememberSaveableSaverLazyListStateFocusRequester| Mistake | Fix |
|---|---|
| Hoisting every local state value to a parent "just in case" | Hoist to the lowest owner that actually reads or writes it |
| Extracting a plain state holder for one boolean | Keep simple private UI state local |
| Putting repository calls or product rules in a Compose state holder | Move that logic to a screen state holder such as a |
| Keeping text or selection local when it drives repository-backed screen state | Move that input to the screen state holder with the business logic |
| Passing a state holder deep into unrelated children | Pass plain values and callbacks unless the child truly coordinates the holder's behavior |
| Treating the holder as a dumping ground for a whole screen | Split by cohesive UI behavior, such as search input, sheet coordination, or list controls |
Calling animation suspend functions from | Use a composition-scoped coroutine |
compose-state-authoringremembercompose-state-holder-ui-splitcompose-side-effectscompose-focus-navigation