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.
I walk through the pitfalls of Compose state when the device rotates, show how to use rememberSaveable, ViewModel, and lifecycle owners to keep UI data intact, and provide real code snippets and tables for quick reference.
On this page
When I first stepped into Compose, I spent hours debugging the same state reset after every rotation. It felt like a broken heartbeat. The good news: Compose offers built‑in tools that, when used correctly, keep your UI state alive across configuration changes. I’ll share the patterns that actually work and how to apply them in a real project.
The Android framework guarantees that an Activity or Fragment is recreated whenever a configuration change occurs—screen rotation, locale change, or keyboard visibility. In the traditional View system, developers relied on
onSaveInstanceStaterememberViewModelrememberrememberSaveableBundle@Composable
fun CounterScreen() {
var count by rememberSaveable { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}[!TIP]
Wrap primitive ortypes incodeParcelable. For complex objects, use acoderememberSaveabledata class or a customcode@Parcelize.codeSaver
| Before | After Configuration Change |
|---|---|
code code | code code |
code code | code code |
[!NOTE]
only works with types that thecoderememberSaveablecan serialize. If you need to preserve custom objects, implement acodeBundle.codeSaver
When the state is a
StateFlowLiveDataViewModelViewModelUsing a
ViewModelViewModelclass CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.update { it + 1 }
}
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsState()
Button(onClick = viewModel::increment) {
Text("Count: $count")
}
}[!IMPORTANT]
Thehelper automatically scopes thecodeviewModel()to the nearestcodeViewModelorcodeNavBackStackEntry, ensuring it survives configuration changes.codeActivity
When the user rotates the device, Compose will recreate the composables, but the
ViewModelcollectAsState()StateFlow| Scenario | Best Practice | Why |
|---|---|---|
| Simple UI counters | code | Minimal boilerplate, no dependencies |
| Network data, business logic | code code | Lifecycle‑aware, testable, single source of truth |
| Shared data between screens | Shared code | Keeps state consistent across navigation |
Compose also offers
DisposableEffectLaunchedEffect@Composable
fun DataFetcher(viewModel: DataViewModel = viewModel()) {
val data by viewModel.data.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadData()
}
// UI here
}[!WARNING]
Forgetting to cancel long‑running operations incan lead to memory leaks or duplicated network calls after rotation.codeDisposableEffect
If you need to preserve a coroutine that must outlive a composable, launch it in the
ViewModelrememberCoroutineScopeAcknowledgementScreen(viewModel: AcknowledgementViewModel = viewModel()) {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
viewModel.saveAcknowledgement()
}
}) {
Text("Accept")
}
}[!NOTE]
Move heavy logic into the. Keep the composable lean, only responsible for UI.codeViewModel
If you need to restore state after a process death, combine
ViewModelSavedStateHandleBundleclass TodoViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _todos = MutableStateFlow(
savedStateHandle.get<List<Todo>>("todos") ?: emptyList()
)
val todos: StateFlow<List<Todo>> = _todos.asStateFlow()
fun addTodo(todo: Todo) {
val updated = _todos.value + todo
_todos.value = updated
savedStateHandle.set("todos", updated)
}
}[!TIP]
Usefor UI‑specific data that should survive process death but is not part of your domain model.codeSavedStateHandle
Below is a minimal Compose app that demonstrates state preservation across rotation, process death, and navigation qualify.
@Composable
fun MainScreen() {
val navController = rememberNavController()
NavHost(navController, startDestination = "counter") {
composable("counter") { CounterScreen() }
composable("details") { DetailScreen() }
}
}
@Composable
fun CounterScreen() {
val viewModel: CounterViewModel = viewModel()
val count by viewModel.count.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center
) {
Text("Count: $count", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = viewModel::increment) {
Text("Increment")
}
}
}
@ strike to comment outclass CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() = _count.update { it + 1 }
}
eriRunning this app, rotating the device leaves the counter intact. If you kill the process (force stop) and reopen the app, the counter resets—because we didn’t persist it. Add a
SavedStateHandleclass CounterViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _count = MutableStateFlow(
savedStateHandle.get<Int>("count") ?: 0
)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
val newValue = _count.value + 1
_count.value = newValue
savedStateHandle.set("count", newValue)
}
}Now the counter survives even a full process kill.
rememberSaveableBundleViewModelStateFlowLiveDataSavedStateHandleDisposableEffectViewModelThese patterns keep your Compose UI stable, testable, and consistent across the unpredictable landscape of Android configuration changes.
Sudarshan 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