Should you migrate from LiveData to Flow?
Where LiveData Falls Short
MediatorLiveData, the guide says, but "the capability to combine streams of data is very limited and all LiveData objects (including ones created through transformations) are observed on the main thread".
Writing is tied to the main thread. Of setValue, the reference says "This method must be called from the main thread."
The background path drops values. Of postValue: "If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched." If your code needs every value, that is a bug waiting for load.
This section used to add claims about buffering strategies, an operator count and Kotlin Multiplatform. None had a source, and they are gone.What changes when you switch
observe with a bare collect and the screen keeps collecting in the background. The documented fix is to collect "from a Lifecycle.repeatOnLifecycle block" in views, and collectAsStateWithLifecycle() in Compose.
LiveData is still fine where a ViewModel exposes one or two simple values with no combining, the team knows the patterns and the screen is stable. That is a cost judgment, ours and not the documentation's.Migration Pattern: ViewModel Layer
switchMap as an extension function. The static Transformations.switchMap(liveData) {...} form that this article used to show was replaced by liveData.switchMap {...} in Lifecycle 2.6, per the conversion table in the Lifecycle release notes. The current stable Lifecycle is 2.11.0.
The after sample needs two opt-ins, because debounce is annotated @FlowPreview and flatMapLatest is @ExperimentalCoroutinesApi. The coroutines guide covers why flatMapLatest is the right operator for search and what WhileSubscribed(5000) does.// BEFORE: LiveData
class SearchViewModel(
private val repo: SearchRepository
) : ViewModel() {
private val _query = MutableLiveData("")
val results: LiveData<List<Result>> = _query.switchMap { q ->
liveData {
if (q.length >= 2) emit(repo.search(q))
}
}
fun onQueryChanged(q: String) {
_query.value = q
}
}
// AFTER: StateFlow
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
class SearchViewModel(
private val repo: SearchRepository
) : ViewModel() {
private val _query = MutableStateFlow("")
val results: StateFlow<List<Result>> = _query
.debounce(300)
.filter { it.length >= 2 }
.flatMapLatest { q ->
flow { emit(repo.search(q)) }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList() // StateFlow needs one
)
fun onQueryChanged(q: String) {
_query.value = q
}
}Collecting Flow in Compose and Fragments
observeAsState() becomes collectAsStateWithLifecycle(), which "collects values from a Flow in a lifecycle-aware manner, allowing your app to conserve app resources" (Android).
In a Fragment or Activity it is more code, and it is the code people leave out. Wrap the collection in repeatOnLifecycle(Lifecycle.State.STARTED). In a Fragment, launch it from viewLifecycleOwner, so the collection follows the view and not the Fragment instance.
A flow built with stateIn and WhileSubscribed also changes how you test: with no collector, the value never moves. The fix is in the testing section of the coroutines guide, and the wider strategy is in Compose testing strategies.// Compose
@Composable
fun SearchScreen(
viewModel: SearchViewModel = hiltViewModel()
) {
// BEFORE (LiveData):
// val results by viewModel.results.observeAsState(emptyList())
// AFTER (StateFlow):
val results by viewModel.results.collectAsStateWithLifecycle()
LazyColumn {
items(results) { result ->
ResultCard(result)
}
}
}
// Fragment
class SearchFragment : Fragment() {
private val viewModel: SearchViewModel by viewModels()
override fun onViewCreated(view: View, savedState: Bundle?) {
super.onViewCreated(view, savedState)
viewLifecycleOwner.lifecycleScope.launch {
// Without this block the flow keeps collecting when stopped
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.results.collect { results ->
adapter.submitList(results)
}
}
}
}
}Incremental Migration Strategy
asFlow() "Creates a Flow containing values dispatched by originating LiveData".
- asLiveData() "Creates a LiveData that has values collected from the origin Flow".
The LiveData guide endorses the second as a pattern for the layers below the UI: "consider using Kotlin Flows and then converting them to LiveData in the ViewModel using asLiveData()". That gives you an order of work. Move repositories and data sources to Flow first, keep the ViewModel's LiveData surface with asLiveData(), then convert the ViewModel and its screen together.
Put screens that fight MediatorLiveData at the front of the queue, since combining streams is where the documented limit bites. New features start on StateFlow from day one.
| LiveData | StateFlow | |
|---|---|---|
| Initial value | Not required | Required |
| Stops when the view stops | Automatically, with observe() | Only inside repeatOnLifecycle or collectAsStateWithLifecycle() |
| Writing from a background thread | postValue, which keeps only the last value | Assign value directly |
| Combining sources | "very limited" | Flow operators such as combine |
| Compose collection | observeAsState() | collectAsStateWithLifecycle() |
// Bridging during incremental migration
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
class HybridViewModel(
private val repo: UserRepository
) : ViewModel() {
// Already migrated to Flow
private val _searchQuery = MutableStateFlow("")
val searchResults: StateFlow<List<User>> = _searchQuery
.debounce(300)
.flatMapLatest { repo.searchUsers(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
// Not yet migrated: still LiveData
private val _selectedTab = MutableLiveData(0)
val selectedTab: LiveData<Int> = _selectedTab
// Bridge: consume the LiveData source as a Flow
val screenState: StateFlow<ScreenState> = combine(
searchResults,
_selectedTab.asFlow()
) { results, tab ->
ScreenState(results, tab)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ScreenState())
}Key Takeaways
- 1Android architecture guidance for new code is a StateFlow collected with collectAsStateWithLifecycle. It no longer mentions LiveData.
- 2Do not rewrite working LiveData screens for their own sake. Migrate where combining streams or background writes hurt.
- 3StateFlow needs an initial value. LiveData did not, so each migrated property needs a real starting state.
- 4A bare collect does not stop when the view stops. Use repeatOnLifecycle in views and collectAsStateWithLifecycle in Compose.
- 5postValue keeps only the last value when called repeatedly before the main thread runs.
- 6asFlow() and asLiveData() bridge the two, so repositories can move to Flow before any screen changes.
Frequently Asked
Should I rewrite all LiveData to Flow?
No. Write new ViewModels with StateFlow, which is what Android architecture guidance now describes, and leave working LiveData screens alone until you have a reason. The reasons worth acting on are concrete: combining several sources, writing from background threads without losing values, or sharing the code with layers that already use Flow.
Is Flow replacing LiveData?
In the guidance, yes. The Android architecture recommendations say to make UI state a StateFlow and to collect it with collectAsStateWithLifecycle, and they no longer mention LiveData. LiveData itself is still shipped in androidx.lifecycle, and the LiveData guide still documents asLiveData() for converting a Flow at the ViewModel boundary.
What is the most common bug when migrating from LiveData to StateFlow?
Replacing observe() with a plain collect. LiveData unregisters its observer when the view stops, and a flow does not stop collecting automatically. The screen keeps working, so nobody notices the background work. Collect inside repeatOnLifecycle in a Fragment or Activity, and use collectAsStateWithLifecycle in Compose.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.