LiveData-to-Flow and Dagger-to-Hilt migration
These migrations change semantics and object graphs, not merely types/annotations.
LiveData to Flow
LiveData.observe is lifecycle-aware. Flow collection is not automatically stopped with the UI. StateFlow requires an initial value and conflates state; an event stream needs a separate delivery contract.
class ProfileViewModel : ViewModel() {
private val _state = MutableStateFlow(ProfileUiState.Loading)
val state: StateFlow<ProfileUiState> = _state.asStateFlow()
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect(::render)
}
}
Audit cold-flow duplicate work, stateIn scope/start policy, replay, error/completion, initial/loading state, equality/conflation, lifecycle, event loss/duplication, and test schedulers. During coexistence expose one canonical producer with adapters, not two independent sources of truth.
Dagger to Hilt
Inventory component scopes, subcomponents, component dependencies, custom factories, multibindings, qualifiers, entry points, test replacements, dynamic features, and startup ordering. Map lifetimes explicitly before replacing annotations.
Migrate leaf bindings and Android entry points incrementally. Use compatibility entry points/adapters where necessary, but keep one owner for each scoped object. Two singleton graphs can create duplicate databases, caches, sessions, or analytics.
Practical mistakes
- ❌ Replace
LiveData<T>withStateFlow<T?> = MutableStateFlow(null)everywhere. - ✅ Model a meaningful initial state and separate events.
- ❌ Collect Flow with an unconstrained UI
launch. - ✅ use lifecycle-aware collection.
- ❌ Run old/new DI containers with duplicate “singletons.”
- ✅ define graph ownership and bridge explicitly.