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.

Introduction

LiveData has been the standard for UI state since Architecture Components launched. Flow offers more operators, better coroutine integration, and multiplatform support. This guide compares both, explains when to migrate, and provides conversion patterns.

LiveData vs Flow: Key Differences

LiveData is lifecycle-aware and Android-specific. Flow is a cold stream requiring lifecycleScope for UI observation. Flow offers 50+ operators vs LiveData's limited transform. Flow works on all Kotlin platforms.

When to Stick with LiveData

Keep LiveData for simple UI state, when team isn't comfortable with coroutines, or for existing stable code. LiveData's lifecycle awareness is automatic and battle-tested.

When to Migrate to Flow

Migrate for complex async operations, when you need operators like debounce/flatMapLatest, for non-UI data streams, or in multiplatform projects. Flow excels at data transformation pipelines.

Migration Patterns

Convert LiveData to Flow with .asFlow(). Use .asLiveData() to expose Flow as LiveData for UI. Use StateFlow as LiveData replacement for state. Use SharedFlow for events.

Frequently Asked Questions

Should I rewrite all LiveData to Flow?

No. Rewrite when you need Flow's capabilities. LiveData remains valid for simple UI state. Incremental migration is better than big-bang rewrite.

Is Flow replacing LiveData?

Not entirely. Google recommends StateFlow/SharedFlow for new development but LiveData remains supported. Flow is preferred for data layers; LiveData is still acceptable for UI.

SYSONLINE
SDK36 · MIN 24
KOTLIN2.0
UTC07:38:05
UP00:01
MOD · ARTICLE · KOTLINS/N · AX-KOTLINPUBLISHED
KotlinFeb 20, 20269 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

The State of Reactive Android in 2026

LiveData was introduced in 2017 as part of Android Architecture Components. It solved a real problem: observing data changes with automatic lifecycle awareness. No more leaked observers, no more updating UI after onDestroy. For years, it was the standard. Kotlin Flow arrived later as part of kotlinx.coroutines. It provides a richer API for asynchronous streams, better composability, and isn't tied to the Android framework. Google's official guidance now recommends StateFlow and SharedFlow over LiveData for new projects. But "new projects" is the key phrase. Most production apps have extensive LiveData usage that works correctly. Migrating for migration's sake wastes engineering time. The question isn't "which is better" but "when does migration deliver enough value to justify the cost."

Where LiveData Falls Short

LiveData has practical limitations that become apparent in complex apps: **No built-in operators.** Transforming LiveData requires Transformations.map() and Transformations.switchMap(), which are limited compared to Flow's dozens of operators. Combining three LiveData sources requires MediatorLiveData with manual wiring. **Main thread only.** LiveData.setValue() must be called from the main thread. Background work requires postValue(), which drops intermediate values if called rapidly. Flow has no such restriction. **No backpressure.** If producers emit faster than consumers can process, LiveData has no strategy beyond dropping values with postValue(). Flow provides buffers, conflation, and configurable overflow strategies. **Platform coupling.** LiveData lives in the androidx.lifecycle package. It can't be used in Kotlin Multiplatform modules, pure Kotlin libraries, or non-Android targets. Flow is pure Kotlin.

Where LiveData Still Works Fine

Not every LiveData usage needs migration. LiveData is perfectly adequate when: - The ViewModel exposes simple state holders with no complex transformations - You are not doing Kotlin Multiplatform - The team is comfortable with LiveData patterns and the codebase is stable - The screen has one or two independent data sources Migrating a working LiveData-based screen to StateFlow just to be "modern" introduces risk without proportional benefit. Prioritize migration where LiveData's limitations cause actual problems.

Migration Pattern: ViewModel Layer

The cleanest migration path replaces LiveData with StateFlow in the ViewModel while keeping the Composable or Fragment observation code nearly identical. StateFlow has the same "hold latest value" semantics as LiveData, making it a direct substitute.
kotlin
// BEFORE: LiveData
class SearchViewModel(
    private val repo: SearchRepository
) : ViewModel() {

    private val _query = MutableLiveData("")
    val query: LiveData<String> = _query

    // MediatorLiveData to combine sources -- verbose
    val results: LiveData<List<Result>> =
        Transformations.switchMap(_query) { q ->
            liveData {
                if (q.length >= 2) {
                    emit(repo.search(q))
                }
            }
        }

    fun onQueryChanged(q: String) {
        _query.value = q
    }
}

// AFTER: StateFlow
class SearchViewModel(
    private val repo: SearchRepository
) : ViewModel() {

    private val _query = MutableStateFlow("")
    val query: StateFlow<String> = _query.asStateFlow()

    // Flow operators: cleaner, more powerful
    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()
        )

    fun onQueryChanged(q: String) {
        _query.value = q
    }
}

Collecting Flow in Compose and Fragments

In Jetpack Compose, replace observeAsState() with collectAsStateWithLifecycle(). In Fragments, replace observe() with a lifecycle-aware collection using repeatOnLifecycle. The critical detail: Flow collection must be lifecycle-aware to avoid processing emissions when the UI is in the background. collectAsStateWithLifecycle() handles this automatically in Compose. In Fragments, use flowWithLifecycle or repeatOnLifecycle.
kotlin
// Compose: almost identical to LiveData observation
@Composable
fun SearchScreen(
    viewModel: SearchViewModel = hiltViewModel()
) {
    // BEFORE (LiveData):
    // val results by viewModel.results.observeAsState(emptyList())

    // AFTER (StateFlow):
    val results by viewModel.results
        .collectAsStateWithLifecycle()

    // Rest of the composable is unchanged
    LazyColumn {
        items(results) { result ->
            ResultCard(result)
        }
    }
}

// Fragment: requires more boilerplate than Compose
class SearchFragment : Fragment() {

    private val viewModel: SearchViewModel by viewModels()

    override fun onViewCreated(
        view: View, savedState: Bundle?
    ) {
        super.onViewCreated(view, savedState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(
                Lifecycle.State.STARTED
            ) {
                viewModel.results.collect { results ->
                    adapter.submitList(results)
                }
            }
        }
    }
}

Incremental Migration Strategy

Migrate incrementally rather than rewriting the entire app at once. Start with new features -- write all new ViewModels with StateFlow. Then migrate existing screens opportunistically when you're already modifying them for a feature change or bug fix. Prioritize migrating screens that currently use MediatorLiveData, Transformations chains, or complex LiveData merging. These are the screens where Flow operators deliver the most readability improvement. You can mix LiveData and Flow in the same ViewModel during migration. The asFlow() and asLiveData() extension functions convert between the two. This lets you migrate one property at a time without breaking the rest of the screen.
CapabilityLiveDataKotlin Flow
Lifecycle AwarenessBuilt-inVia collectAsStateWithLifecycle()
Operatorsmap, switchMap only30+ (map, filter, combine, flatMapLatest, debounce, etc.)
ThreadingMain thread only (setValue)Any dispatcher
BackpressureDrops via postValueConfigurable (buffer, conflate, drop)
MultiplatformAndroid onlyPure Kotlin — works everywhere
CompositionMediatorLiveData (manual)combine, zip, merge (declarative)
TestingInstantTaskExecutorRulerunTest + TestDispatcher
Cold vs HotAlways hotCold by default, hot via StateFlow/SharedFlow
kotlin
// Bridging during incremental migration
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.Lazily,
            emptyList()
        )

    // Not yet migrated -- still LiveData
    private val _selectedTab = MutableLiveData(0)
    val selectedTab: LiveData<Int> = _selectedTab

    // Bridge: consume a LiveData source as Flow
    val combinedState: StateFlow<ScreenState> = combine(
        searchResults,
        _selectedTab.asFlow()  // LiveData -> Flow bridge
    ) { results, tab ->
        ScreenState(results, tab)
    }.stateIn(
        viewModelScope,
        SharingStarted.Lazily,
        ScreenState()
    )
}
MOD · TAKEAWAYS6 POINTSSUMMARY

Key Takeaways

  1. 1StateFlow replaces LiveData with richer operators, no main-thread restriction, and Kotlin Multiplatform support.
  2. 2Don't migrate working LiveData code without a concrete reason -- migration has a cost.
  3. 3Prioritize migration for screens with complex MediatorLiveData or Transformations chains.
  4. 4Use collectAsStateWithLifecycle() in Compose, repeatOnLifecycle in Fragments.
  5. 5asFlow() and asLiveData() bridge the two systems during incremental migration.
  6. 6SharingStarted.WhileSubscribed(5000) is the standard for screen-level StateFlow.
MOD · FAQ2 ENTRIESANSWERED

Frequently Asked

Should I rewrite all LiveData to Flow?

No. Rewrite when you need Flow's capabilities. LiveData remains valid for simple UI state. Incremental migration is better than big-bang rewrite.

Is Flow replacing LiveData?

Not entirely. Google recommends StateFlow/SharedFlow for new development but LiveData remains supported. Flow is preferred for data layers; LiveData is still acceptable for UI.

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