SYSONLINE
SDK36 · MIN 24
KOTLIN2.0
UTC22:23:07
UP00:01
MOD · ARTICLE · KOTLINS/N · AX-KOTLINPUBLISHED
All Articles
Kotlin · Updated 9 MIN

Kotlin Flow vs LiveData: When to Migrate and How

Understand the practical differences between Flow and LiveData, when migration makes sense, and how to convert your codebase incrementally.

By Rocky Elsalaymeh · Founder & Principal Consultant, Strategia-X

Should you migrate from LiveData to Flow?

LiveData solved a real problem: observing data from a screen without leaking the observer or updating a view that is gone. Most production apps still have a lot of it, and it still works. What changed is where Android's own guidance points. The architecture recommendations no longer mention LiveData at all. They say "Make uiState a StateFlow" and, marked strongly recommended, "Collect UI state from the UI using the appropriate lifecycle-aware coroutine builder, collectAsStateWithLifecycle". The Compose documentation calls that API "the recommended way to collect flows on Android apps" (Android). An earlier version of this article said Google recommends StateFlow over LiveData and linked a kotlinlang.org page as proof. That page returns 404. The sentences above are what the documentation actually says, and they are about new code. So the answer has two halves. Write new ViewModels with StateFlow. Do not rewrite a working LiveData screen for its own sake. Migrate where LiveData's limits are costing you something, and the next section says what those limits are.

Where LiveData Falls Short

The LiveData guide is direct about the boundary: "LiveData is not designed to handle asynchronous streams of data." Three limits follow, each stated by Android's own documentation. Combining is limited and everything runs on the main thread. You can use transformations and 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

StateFlow is close enough to LiveData that the swap looks mechanical. Android's comparison names the two differences that make it not mechanical (Android): - "StateFlow requires an initial state to be passed in to the constructor, while LiveData does not." Every migrated property needs a real initial value, which usually means modeling a loading state you were getting for free from an empty LiveData. - "LiveData.observe() automatically unregisters the consumer when the view goes to the STOPPED state, whereas collecting from a StateFlow or any other flow does not stop collecting automatically." The second one is the migration bug. Replace 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

Start in the ViewModel. The before sample is written the way current LiveData code should be: 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.
kotlin
// 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

In Compose the change is one line. 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.
kotlin
// 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

Migrate a property at a time. Two extension functions bridge the systems in both directions, and both are in the androidx.lifecycle reference: - 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.
LiveDataStateFlow
Initial valueNot requiredRequired
Stops when the view stopsAutomatically, with observe()Only inside repeatOnLifecycle or collectAsStateWithLifecycle()
Writing from a background threadpostValue, which keeps only the last valueAssign value directly
Combining sources"very limited"Flow operators such as combine
Compose collectionobserveAsState()collectAsStateWithLifecycle()
kotlin
// 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())
}
MOD · TAKEAWAYS6 POINTSSUMMARY

Key Takeaways

  1. 1Android architecture guidance for new code is a StateFlow collected with collectAsStateWithLifecycle. It no longer mentions LiveData.
  2. 2Do not rewrite working LiveData screens for their own sake. Migrate where combining streams or background writes hurt.
  3. 3StateFlow needs an initial value. LiveData did not, so each migrated property needs a real starting state.
  4. 4A bare collect does not stop when the view stops. Use repeatOnLifecycle in views and collectAsStateWithLifecycle in Compose.
  5. 5postValue keeps only the last value when called repeatedly before the main thread runs.
  6. 6asFlow() and asLiveData() bridge the two, so repositories can move to Flow before any screen changes.
MOD · FAQ3 ENTRIESANSWERED

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.

MOD · BUILD · NEXTS/N · AX-CTA-0001READY

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.

ANDROID-ARCHITECT · CONSOLE
S/N · AX-1A-00001
STRATEGIA-X · ENGINEERED · IN · CALIFORNIA