On this page
In Compose, the UI is derived from state. When it changes, affected composables recompose. That simple idea raises practical questions: where should each piece of state live? Should it survive rotation? Process death? Should it be shared? Answer those correctly and Compose feels effortless.
The kinds of state in a screen
| Kind | Example | Owner | API |
|---|---|---|---|
| UI element state | Is this card expanded? Text field focus | The composable, or a plain state-holder class | remember / rememberSaveable |
| Screen UI state | Loaded articles, loading flag, error message | ViewModel | StateFlow + collectAsStateWithLifecycle |
| User input to preserve | Draft text, selected filters | ViewModel with SavedStateHandle, or rememberSaveable | Survives process death |
| App data | Notes in the database, settings | Repository (Room, DataStore) | Flow exposed upward |
Local state with remember
@Composable
fun ExpandableCard(title: String, body: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) } // survives rotation
Card(modifier.fillMaxWidth().clickable { expanded = !expanded }) {
Column(Modifier.padding(16.dp).animateContentSize()) {
Text(title, style = MaterialTheme.typography.titleMedium)
if (expanded) Text(body, Modifier.padding(top = 8.dp))
}
}
}mutableStateOf creates an observable value. remember keeps it across recompositions. Without remember, every recomposition would create a fresh false.
Screen state in a ViewModel
data class InboxUiState(
val messages: List<Message> = emptyList(),
val filter: Filter = Filter.ALL,
val isRefreshing: Boolean = false,
)
class InboxViewModel(repo: MessageRepository, private val saved: SavedStateHandle) : ViewModel() {
private val filter = saved.getStateFlow("filter", Filter.ALL) // restored after process death
private val refreshing = MutableStateFlow(false)
val uiState: StateFlow<InboxUiState> =
combine(repo.observeMessages(), filter, refreshing) { msgs, f, r ->
InboxUiState(msgs.filter(f::matches), f, r)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), InboxUiState())
fun setFilter(f: Filter) { saved["filter"] = f }
}
@Composable
fun InboxRoute(viewModel: InboxViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
InboxScreen(state, onFilterChange = viewModel::setFilter)
}Filter needs to be storable in a Bundle, for example as an enum, for SavedStateHandle to save it.
Plain state-holder classes for complex UI logic
When a component's UI logic grows (several related state values and rules), move it into a plain class remembered in composition. It's simpler than a ViewModel and still testable.
@Stable
class SearchBarState(initialQuery: String) {
var query by mutableStateOf(initialQuery)
private set
var active by mutableStateOf(false)
private set
val showClear: Boolean get() = query.isNotEmpty()
fun onQueryChange(q: String) { query = q.take(100) }
fun clear() { query = "" }
fun activate(value: Boolean) { active = value }
}
@Composable
fun rememberSearchBarState(initialQuery: String = ""): SearchBarState =
remember { SearchBarState(initialQuery) }Compose's own LazyListState, ScrollState and DrawerState follow exactly this pattern.
Derived state
val listState = rememberLazyListState()
// Recompose readers only when the boolean flips, not on every scroll pixel.
val showJumpToTop by remember { derivedStateOf { listState.firstVisibleItemIndex > 3 } }Use derivedStateOf when the input changes more often than the result. For simple derivations from parameters, plain computation, or remember(key) for expensive work, is enough.
Turning Compose state into a Flow
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index -> analytics.log("scrolled_to", index) }
}Real-world decisions
Checkout form
Field values and validation live in the ViewModel, with in-progress input in SavedStateHandle so process death doesn't wipe the form.
Photo carousel
The current page index is UI element state in rememberPagerState, hoisted only if a parent needs to show "3 of 10".
Settings toggles
App data in DataStore, exposed as a Flow and mapped to UI state in the ViewModel. Toggles call ViewModel functions.
Common mistakes
mutableStateOfwithoutremember, so state resets.- Using
collectAsState()wherecollectAsStateWithLifecycle()is better on Android, since it stops collecting in the background. - Duplicating state, for example a copy of ViewModel data in a local
rememberthat goes out of sync. - Mutable collections inside state (
mutableListOfin amutableStateOf). Changes inside the list aren't observed. Use immutable lists with copy, ormutableStateListOf. - Writing state during composition, which causes infinite recomposition loops.
Frequently asked questions
Do I need a ViewModel for every screen?
For screens with business logic or data loading, yes. Purely presentational screens can rely on local state and parameters.
StateFlow or Compose State in the ViewModel?
Both work. StateFlow keeps ViewModels independent of Compose and integrates with Flow operators, so it's the most common choice. Compose mutableStateOf in ViewModels is also supported.
How do I share state between two screens?
Hoist it to a common owner, such as a ViewModel scoped to a navigation graph or the Activity, or move it into the data layer where both screens observe it.
Conclusion
Good Compose state management is about ownership: UI element state in remember/rememberSaveable or state-holder classes, screen state in a ViewModel's StateFlow, preserved input in SavedStateHandle, and app data in repositories. Keep a single source of truth for each value and let the UI derive from it.
Next step: get the survival rules exactly right with remember vs rememberSaveable.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Jetpack Compose clicks once you build real screens with real state. Practise modern Android UI development on projects that go beyond toy examples.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.