Ditching LiveData: StateFlow and SharedFlow as the Kotlin-first replacement, with a migration playbook
Google's own guidance and the widening Kotlin ecosystem are moving Android state management away from LiveData toward Flow-based tools. Teams on older LiveData stacks face a concrete, incremental migration path that removes single-observer event hacks and unlocks compositional stream operators.
LiveData's design from the Java era is showing its limits in Kotlin-first Android development. It lacks native coroutine integration, offers only a handful of operators (map, switchMap), forces main-thread callbacks, and its sticky behavior makes it a poor fit for one-time events. Flow, Kotlin's own reactive stream, provides hundreds of operators, arbitrary thread control, and explicit hot/cold flow semantics. StateFlow and SharedFlow are the practical replacements: StateFlow is a state holder with a mandatory initial value and automatic deduplication; SharedFlow makes sticky behavior configurable via the replay parameter, enabling multi-observer one-time events without SingleLiveEvent hacks.
Lifecycle safety is handled by repeatOnLifecycle, the official extension that starts and cancels collection as the Activity/Fragment enters and leaves STARTED. It replaces the leak-prone launchWhenStarted pattern, which suspends callbacks but leaves the flow running. The migration path is incremental: convert ViewModel fields first, switch UI collection from observe to lifecycleScope + repeatOnLifecycle, and use Flow.asLiveData()/liveData.asFlow() for mixed codebases. A complete refactor of a note-taking app demonstrates debounce, distinctUntilChanged, flatMapLatest, and stateIn with SharingStarted.WhileSubscribed.
StateFlow's mandatory initial value pushes teams to model UI state with sealed classes, which is stricter than LiveData's nullable approach and often prevents null-state logic bugs.
The shift to Flow makes thread switching and stream composition explicit in the type system, so code becomes more self-documenting about where I/O happens.
SharingStarted.WhileSubscribed(5000) keeps upstream resources alive for five seconds after the last collector leaves, a practical balance between re-creation overhead and wasted work.
The migration strategy's pragmatism (mixing LiveData and Flow via conversion functions) acknowledges that replacing all LiveData in one sprint is unrealistic for mature apps.