MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
How I think about state in Compose as screens grow — from a single remembered value to hoisted state, ViewModel-backed StateFlow, and the rules that keep recomposition cheap and bugs rare.
On this page
State is the part of Compose that looks easy in the docs and gets messy in real apps. A counter is trivial. A screen with a form, async loading, validation, and a snackbar is where most state bugs live. After building this same pattern across a couple dozen apps, I've settled on a progression: start as simple as the screen allows, and only climb the ladder when the screen forces you to.
If a piece of state never leaves a single composable, keep it there.
@Composable
fun PasswordField() {
var visible by remember { mutableStateOf(false) }
// toggle visible; nobody outside this composable cares
}rememberrememberSaveableThe moment a parent needs to read or control that state, hoist it. A composable that takes its state as a parameter and emits changes as a callback is testable, previewable, and reusable.
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
) {
TextField(value = query, onValueChange = onQueryChange)
}The rule of thumb: state goes to the lowest common ancestor that needs it, and no higher. Hoist too far and every keystroke recomposes half the tree. Hoist too little and you can't share it.
Once state must survive navigation, involve business logic, or come from a repository, it belongs in a ViewModel exposed as
StateFlowLiveDataStateFlowclass ProfileViewModel(repo: UserRepository) : ViewModel() {
private val _state = MutableStateFlow(ProfileUiState())
val state: StateFlow<ProfileUiState> = _state.asStateFlow()
fun onNameChange(name: String) {
_state.update { it.copy(name = name) }
}
}In the UI, collect it lifecycle-aware:
val state by viewModel.state.collectAsStateWithLifecycle()collectAsStateWithLifecyclecollectAsStateA screen with five
mutableStateOfsealed interface ProfileUiState {
data object Loading : ProfileUiState
data class Error(val message: String) : ProfileUiState
data class Ready(val name: String, val email: String) : ProfileUiState
}Now the UI is a
whenComplex state is fine; expensive recomposition is not. Two habits prevent most jank:
derivedStateOfkotlinx.collections.immutableval canSubmit by remember {
derivedStateOf { state.name.isNotBlank() && state.email.contains("@") }
}The reason state management feels overwhelming is that people treat it as one big problem instead of a ladder of small ones. Every screen sits on exactly one rung, and the rung is decided by a single question: who else needs to see or change this value? A toggle that only one widget reads stays local. A search query a parent filters on gets hoisted. A user profile that must survive rotation, come from the network, and feed three screens lives in a ViewModel. You don't choose the rung by taste; the screen's requirements choose it for you, and your job is just to read them honestly.
The failure mode I see most often is climbing too high too early. A developer reads that ViewModels are the "right" way and routes a checkbox through one, and now a trivial interaction involves a flow, a reducer, and three files. The opposite failure is climbing too late: cramming network state and validation into scattered
mutableStateOfWhat keeps this sane over dozens of apps is that the ladder is the same every time. I never reinvent how a screen holds state; I just identify the rung and apply the matching pattern. New screens become boring, and boring is exactly what you want from infrastructure. The interesting part of the app should be the feature, not the plumbing that holds its state. When state management is a decision you make in ten seconds instead of an architecture debate, you ship faster and you debug less, because the structure tells you where every value lives before you go looking.
rememberrememberSaveableStateFlowLiveDatacollectAsStateWithLifecyclederivedStateOfSudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read