Mobile Development 17 min read

Where Should State Live in Jetpack Compose? Practical Guidelines

This guide explains how to decide whether a piece of state belongs in a Composable or a ViewModel by distinguishing UI state from business state, using clear principles, lifecycle considerations, and concrete code examples such as search bars, expandable cards, and drag‑and‑drop scenarios.

AndroidPub
AndroidPub
AndroidPub
Where Should State Live in Jetpack Compose? Practical Guidelines

Key Decision Principle

State that influences business logic should go to a ViewModel; state that does not affect business logic should stay as close as possible to where it is used.

This simple rule helps solve many real‑world state‑ownership problems by focusing on the nature of the state rather than applying a one‑size‑fits‑all rule.

Why Misplacing State Increases Complexity

Unnecessary upward movement creates extra coupling between UI and business layers.

Longer read‑modify chains make it harder to understand which component changes a state and why.

Mixing UI and business state causes changes on one side to unintentionally affect the other, blurring boundaries.

When many roles share the same state, reasoning about the flow becomes difficult, leading to harder maintenance and reduced extensibility.

Keep State Close to Its Usage

Unless there is a clear reason, a state should remain in the nearest composable. Pure UI state is usually stored with remember or rememberSaveable so that the data and its UI logic stay together.

@Composable
fun SearchBar() {
    var query by rememberSaveable { mutableStateOf("") }

    TextField(
        value = query,
        onValueChange = { query = it },
        label = { Text("Search") }
    )
}

If the state only drives the current screen's input behavior, keeping it locally is simpler and more direct.

Business State vs. UI State

Business State

State that influences business logic, determines outcomes, and often needs to survive configuration changes. Typical characteristics:

Impacts domain‑logic decisions.

Determines the result of a user action.

Must be observable outside the UI layer.

Usually has a longer lifecycle.

Example: a search query that triggers data loading and affects which results are displayed.

class SearchViewModel : ViewModel() {
    var query by mutableStateOf("")
        private set

    fun onQueryChange(newQuery: String) {
        query = newQuery
        loadResults(newQuery)
    }

    private fun loadResults(query: String) {
        // Execute business logic based on the query
    }
}

UI State

State that only supports visual presentation or interaction, does not affect business outcomes, and usually lives only within a composable.

Changes only the display or interaction experience.

Never alters business results.

Scoped to the current composable or UI region.

Short‑lived and temporary.

Example: an expandable card’s expanded flag.

@Composable
fun ExpandableCard() {
    var expanded by remember { mutableStateOf(false) }

    Column {
        Button(onClick = { expanded = !expanded }) {
            Text(if (expanded) "Hide" else "Show")
        }
        if (expanded) {
            Text("Details")
        }
    }
}

Typical Drag‑and‑Drop Scenario

The final bucket assignment is business state and belongs in a ViewModel, while the real‑time drag offset is UI state and stays in the composable.

@Composable
fun DraggableItem(onDropped: (Bucket) -> Unit) {
    var offset by remember { mutableStateOf(Offset.Zero) }

    Box(
        modifier = Modifier
            .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
            .pointerInput(Unit) {
                detectDragGestures(
                    onDrag = { _, dragAmount -> offset += dragAmount },
                    onDragEnd = {
                        onDropped(Bucket.Done)
                        offset = Offset.Zero
                    }
                )
            }
    )
}
class BoardViewModel : ViewModel() {
    fun moveItemToBucket(bucket: Bucket) {
        // Handle the final business result
    }
}

This split keeps the composable focused on interaction and the ViewModel on business consequences.

Lifecycle as an Additional Signal

Business state often needs to survive configuration changes or survive beyond the composable’s lifetime, whereas UI state is short‑lived and should be created and destroyed with the composable.

When to Hoist State

State hoisting is valuable when a state must be shared across multiple children or controlled by a parent. Example of extracting a search query:

@Composable
fun SearchInput(query: String, onQueryChange: (String) -> Unit) {
    TextField(
        value = query,
        onValueChange = onQueryChange,
        label = { Text("Search") }
    )
}
@Composable
fun SearchScreen() {
    var query by rememberSaveable { mutableStateOf("") }

    Column {
        SearchInput(query = query, onQueryChange = { query = it })
        SearchResultList(query = query)
    }
}

If the query later needs to drive network requests, it can be moved to a ViewModel without rewriting the input component.

Common Pitfall: Hoisting Too Early

Putting every piece of state into a ViewModel—such as scroll position, animation progress, or temporary drag coordinates—adds unnecessary coupling and complexity because these states do not influence business logic.

Checklist for State Placement

Does it affect business logic? If yes, consider ViewModel; otherwise, keep locally.

Who actually uses the state? If only local UI, keep in the composable; if multiple flows need it, hoist.

Is its lifecycle short or long? Short‑lived states are UI state; long‑lived states are business state.

Is the hoist solving a real problem or just a precaution? Hoist only when there is a clear sharing or business need.

Conclusion

Effective Compose state management starts by asking a few simple questions: Does the state influence business logic? Who needs it? How long must it live? Is hoisting solving a concrete issue or pre‑emptively adding complexity? Answering these consistently leads to clearer architecture and easier maintenance.

UI state stays close, business state moves up.
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ViewModelAndroidstate-managementJetpack ComposeBusiness StateState HoistingUI State
AndroidPub
Written by

AndroidPub

Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.