The State of Reactive Android in 2026
Where LiveData Falls Short
Where LiveData Still Works Fine
Migration Pattern: ViewModel Layer
// 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
// 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
| Capability | LiveData | Kotlin Flow |
|---|---|---|
| Lifecycle Awareness | Built-in | Via collectAsStateWithLifecycle() |
| Operators | map, switchMap only | 30+ (map, filter, combine, flatMapLatest, debounce, etc.) |
| Threading | Main thread only (setValue) | Any dispatcher |
| Backpressure | Drops via postValue | Configurable (buffer, conflate, drop) |
| Multiplatform | Android only | Pure Kotlin — works everywhere |
| Composition | MediatorLiveData (manual) | combine, zip, merge (declarative) |
| Testing | InstantTaskExecutorRule | runTest + TestDispatcher |
| Cold vs Hot | Always hot | Cold by default, hot via StateFlow/SharedFlow |
// 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()
)
}Key Takeaways
- 1StateFlow replaces LiveData with richer operators, no main-thread restriction, and Kotlin Multiplatform support.
- 2Don't migrate working LiveData code without a concrete reason -- migration has a cost.
- 3Prioritize migration for screens with complex MediatorLiveData or Transformations chains.
- 4Use collectAsStateWithLifecycle() in Compose, repeatOnLifecycle in Fragments.
- 5asFlow() and asLiveData() bridge the two systems during incremental migration.
- 6SharingStarted.WhileSubscribed(5000) is the standard for screen-level StateFlow.
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.
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.