Where and How to Convert to Flow in Android
From Data Sources to UI State: Best Practices for Reactive Streams

1. Data Layer (Repository / DataSource): Generating Cold Flows
- Encapsulating database changes, network sockets, and OS callbacks into cold streams.
- Wrap database change monitoring (such as Room or SQLDelight), WebSockets, or callbacks (like Location) into cold Flows using builders or
callbackFlowto expose them.
class UserRepository(private val userDao: UserDao) {
fun observeUser(userId: String): Flow<UserEntity?> {
return userDao.observeUser(userId)
}
}
2. Domain Layer (Use Case): Combining and Transforming Streams
- Merging and processing multiple streams according to business rules.
- Combine and process Flows obtained from multiple data sources according to business logic using operators like
combineorflatMapLatest, then pass them upstream.
class GetUserProfileUseCase(
private val userRepository: UserRepository,
private val settingsRepository: SettingsRepository
) {
operator fun invoke(userId: String): Flow<UserProfile> {
return combine(
userRepository.observeUser(userId),
settingsRepository.observeThemeSetting()
) { user, theme ->
UserProfile(user = user, isDarkTheme = theme.isDark)
}
}
}
3. Presentation Layer (ViewModel): Converting to Hot StateFlow
- Holding resilient screen state across configuration changes using
stateIn. - Convert cold Flows into hot
StateFlow(orSharedFlow) using thestateInoperator to hold and provide them as screen state (UiState), ensuring resilience against configuration changes like screen rotations.
class UserViewModel(
getUserProfileUseCase: GetUserProfileUseCase,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val userId: String = checkNotNull(savedStateHandle["userId"])
val uiState: StateFlow<UserUiState> = getUserProfileUseCase(userId)
.map { profile -> UserUiState.Success(profile) }
.catch { throwable -> emit(UserUiState.Error(throwable.message)) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = UserUiState.Loading
)
}
4. UI Layer (Jetpack Compose): Collecting and Consuming
- Safely observing lifecycle-aware states for pure rendering.
- Collect flows safely tied to the lifecycle using
collectAsStateWithLifecycle(), keeping the UI layer focused purely on rendering without performing any new Flow transformations.
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
is UserUiState.Loading -> CircularProgressIndicator()
is UserUiState.Success -> Text(text = "Hello, ${state.profile.user?.name}")
is UserUiState.Error -> Text(text = "Error: ${state.message}")
}
}
Summary
-
Data Layer: Emit cold Flows from local data sources or asynchronous callbacks.
-
Domain Layer: Combine and transform streams using operators without holding state.
-
ViewModel: Convert cold streams into hot
StateFlowviastateInto maintain UI state across lifecycle events. -
UI Layer: Collect cleanly with
collectAsStateWithLifecycle()to render purely declarative UI.