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.
The switch from LiveData to StateFlow was not a trend decision. It was driven by a specific crash pattern. Here is what LiveData was getting wrong, what StateFlow changed, and what it costs.
On this page
Around app number eight, I had a crash that took three days to trace. The stack trace pointed at a LiveData observer. The observer was receiving stale data in a newly created screen. The screen had been resumed, but the observer was firing before the ViewModel had finished emitting the correct state.
The root cause was observer lifecycle management. LiveData's observer callbacks fire based on lifecycle state changes. Under the right sequence of
onStartonStoponResumeI switched the entire fleet to StateFlow. Here is what changed.
LiveData wraps its observer callbacks in lifecycle awareness automatically. When you call
observe(viewLifecycleOwner, observer)The problem is that this management is implicit. You don't see it. The observer just fires, and you trust that it fires at the right time.
Under normal conditions, this works. Under specific conditions — particularly fragment back-stack management and configuration changes in close succession — the implicit lifecycle management produces observer callbacks in an order that violates the assumptions the UI code was written with.
The crash manifested as: screen A → navigate to screen B → navigate back to screen A → UI shows state from screen B briefly before correcting.
The cause: screen A's LiveData observer received a delivery from a previous emission that was queued during the lifecycle transition. LiveData's
onActiveThis is a known LiveData behavior. It is not a bug. It is how LiveData works. But it produced a real crash in production.
StateFlow is not lifecycle-aware by itself. You collect it explicitly using
repeatOnLifecycle// LiveData — implicit lifecycle management
viewModel.uiState.observe(viewLifecycleOwner) { state ->
render(state)
}
// StateFlow — explicit lifecycle management
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
render(state)
}
}
}The explicit management is more code. It is also auditable. When something is wrong with the observation lifecycle, the failure is visible at the collection point — in code you wrote, in a scope you control.
repeatOnLifecycle(Lifecycle.State.STARTED)LiveData has a
MutableLiveDataMutableStateFlow// LiveData
private val _uiState = MutableLiveData<UiState>()
val uiState: LiveData<UiState> = _uiState
// StateFlow
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()The initial value requirement is the most important difference. StateFlow requires you to define what the state is before any data arrives. This forces an explicit loading/empty/error state model. You can't leave the state undefined.
This is a constraint that improves the code. LiveData allows you to emit nothing until you have something. StateFlow requires you to decide what "nothing" looks like. That decision produces better UI states.
StateFlow is a Kotlin Flow, which means the full Flow operator set is available:
// Combine two StateFlows
val combinedState = combine(
viewModel.userState,
viewModel.locationState
) { user, location ->
CombinedState(user, location)
}
// Transform with debounce for search
val searchResults = searchQuery
.debounce(300)
.flatMapLatest { query ->
repository.search(query)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())LiveData has
Transformations.mapTransformations.switchMapstateInval locations: StateFlow<List<Location>> = repository.getLocations()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)SharingStarted.WhileSubscribed(5000)StateFlow has real costs that LiveData doesn't:
No built-in Activity/Fragment support.
observe()repeatOnLifecycleEquality check skips duplicate emissions. StateFlow uses
equals()SharedFlow for one-shot events. StateFlow holds and replays its current value to new collectors. For one-shot UI events (navigation, toast messages), replaying the last value is wrong. SharedFlow with
replay = 0private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
// Emit from ViewModel
viewModelScope.launch {
_events.emit(UiEvent.NavigateTo(Screen.Detail))
}This is more infrastructure than LiveData's
SingleLiveEventSince switching the fleet to StateFlow: zero crashes attributable to observer lifecycle management. The explicit collection pattern makes lifecycle-related bugs visible in code review rather than visible in production. The Flow operator set reduced a significant amount of LiveData transformation boilerplate.
The switch was not a rewrite. Each screen's ViewModel was updated incrementally — change the
MutableLiveDataMutableStateFlowLiveData still works. It is a stable, well-understood API with good lifecycle integration. But StateFlow's explicit lifecycle management, Flow operator composability, and requirement for initial state produce code that is easier to reason about and harder to break in unexpected ways. For a fleet of 22+ apps where any one of them needs to be debuggable without context reload, that matters.
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