How Data Flows from Repository to UI in Jetpack Compose
Problem
When I started learning Jetpack Compose architecture, I got confused about how data moves between layers. I saw code with Flow, StateFlow, collectAsState(), and MutableStateFlow. I didn’t know where each belongs or how they connect.
I tried to follow tutorials, but they either skipped the details or showed everything at once without explaining the transformation points.
Environment
- Kotlin 1.9
- Jetpack Compose BOM 2024.x
- Hilt for dependency injection
- Room database
- Coroutines and Flow
What happened?
I was building a task list app. I created a Repository, a ViewModel, and a Compose UI. But when I ran the app, the UI showed nothing. No tasks loaded. No errors either.
Here’s my setup:
// Repository returns Flowclass TaskRepository @Inject constructor( private val taskDao: TaskDao) { fun getTasks(): Flow<List<Task>> { return taskDao.getAllTasks() .map { entities -> entities.map { it.toDomain() } } }}// ViewModel should expose StateFlow@HiltViewModelclass TaskListViewModel @Inject constructor( private val repository: TaskRepository) : ViewModel() {
private val _uiState = MutableStateFlow<TaskListUiState>(TaskListUiState.Loading) val uiState: StateFlow<TaskListUiState> = _uiState.asStateFlow()
init { loadTasks() }
fun loadTasks() { // How do I connect repository to uiState? }}@Composablefun TaskListScreen( viewModel: TaskListViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsState()
// How to render based on state?}The core problem: I didn’t understand how data transforms between Repository (Flow) and ViewModel (StateFlow) and finally UI (State).
How to solve it?
I tried connecting Repository to ViewModel directly:
fun loadTasks() { // This doesn't work - Flow is cold! val tasks = repository.getTasks() _uiState.value = TaskListUiState.Success(tasks)}Nothing happened. The Flow didn’t emit any data because it’s cold - it only emits when collected.
I tried collecting the Flow:
fun loadTasks() { viewModelScope.launch { repository.getTasks().collect { tasks -> _uiState.value = TaskListUiState.Success(tasks) } }}This worked! But I was confused about the difference between collect() and collectAsState() in the UI.
Then I discovered stateIn() as an alternative:
val uiState: StateFlow<TaskListUiState> = repository.getTasks() .map { tasks -> TaskListUiState.Success(tasks) as TaskListUiState } .catch { e -> emit(TaskListUiState.Error(e.message ?: "Unknown error")) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = TaskListUiState.Loading )This is cleaner for simple cases. But manual collection gives more control over error handling.
Now let me show the complete working solution:
@HiltViewModelclass TaskListViewModel @Inject constructor( private val repository: TaskRepository) : ViewModel() {
private val _uiState = MutableStateFlow<TaskListUiState>(TaskListUiState.Loading) val uiState: StateFlow<TaskListUiState> = _uiState.asStateFlow()
init { observeTasks() }
private fun observeTasks() { viewModelScope.launch { repository.getTasks() .catch { e -> _uiState.value = TaskListUiState.Error(e.message ?: "Unknown error") } .collect { tasks -> _uiState.value = TaskListUiState.Success(tasks) } } }
fun onTaskToggle(taskId: String) { viewModelScope.launch { repository.toggleTaskCompletion(taskId) // No need to manually refresh - Flow auto-emits new data } }}
sealed interface TaskListUiState { data object Loading : TaskListUiState data class Success(val tasks: List<Task>) : TaskListUiState data class Error(val message: String) : TaskListUiState}@Composablefun TaskListScreen( viewModel: TaskListViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsState()
LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { when (val state = uiState) { is TaskListUiState.Loading -> { item { Box( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } } is TaskListUiState.Success -> { items(state.tasks) { task -> TaskItem( task = task, onToggle = { viewModel.onTaskToggle(task.id) } ) } } is TaskListUiState.Error -> { item { Text( text = "Error: ${state.message}", color = MaterialTheme.colorScheme.error ) } } } }}You can see that I succeeded to load and display tasks. The UI updates automatically when database changes.
The data flow
I want to show the complete data flow diagram:
┌─────────────────────────────────────────────────────────┐│ USER INTERACTION │└─────────────────────────┬───────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────┐│ UI LAYER (Compose) ││ collectAsState() -> State ││ Renders based on state │└─────────────────────────┬───────────────────────────────┘ ▲ │ StateFlow<UiState> │┌─────────────────────────┴───────────────────────────────┐│ VIEWMODEL LAYER ││ - Holds StateFlow (hot) ││ - Collects Repository Flow ││ - Exposes immutable state to UI │└─────────────────────────┬───────────────────────────────┘ ▲ │ Flow<T> │┌─────────────────────────┴───────────────────────────────┐│ REPOSITORY LAYER ││ - Exposes Flow (cold) ││ - Single source of truth ││ - Combines data sources (API + DB) │└─────────────────────────┬───────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────┐│ DATA SOURCES ││ - Remote API ││ - Local Database (Room) │└─────────────────────────────────────────────────────────┘The flow has two directions:
Downward (Data):
- Repository exposes
Flow<T>(cold) - ViewModel collects Flow and emits to
StateFlow<UiState>(hot) - UI observes StateFlow via
collectAsState() - UI recomposes when state changes
Upward (Events):
- User interacts with UI
- UI calls ViewModel function
- ViewModel calls Repository suspend function
- Repository updates data source
- Flow emits new value, cycle repeats
The reason
I think the key reason I struggled is that I missed the transformation points:
Point 1: Flow vs StateFlow
┌─────────────────────────────────────────────────────────┐│ FLOW (Cold) ││ - Only emits when collected ││ - Multiple collectors possible ││ - No initial value ││ - Use in Repository │└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐│ STATEFLOW (Hot) ││ - Always has a value ││ - Caches latest for new collectors ││ - Holds state for UI ││ - Use in ViewModel │└─────────────────────────────────────────────────────────┘Point 2: Repository to ViewModel conversion
The Repository returns cold Flow because it’s just a data source. The ViewModel converts it to hot StateFlow because it needs to cache state for the UI.
Point 3: ViewModel to UI connection
collectAsState() converts StateFlow to Compose State. This is lifecycle-aware - it collects when the composable is active and cancels when it’s destroyed.
Bidirectional flow example
Let me show how user actions flow back up:
USER CLICKS TASK │ ▼UI -> viewModel.onTaskToggle(taskId) │ ▼ViewModel -> repository.toggleTaskCompletion(taskId) │ ▼Repository -> taskDao.update(...) │ ▼Database emits new value via Flow │ ▼ViewModel.collect() receives update │ ▼StateFlow<UiState> emits new state │ ▼collectAsState() triggers recomposition │ ▼UI updates with new dataNo manual refresh needed. The reactive chain handles it.
Summary
In this post, I showed how data flows from Repository to UI in Jetpack Compose. The key point is understanding the transformation points: Repository exposes cold Flow, ViewModel converts to hot StateFlow for caching, and UI observes via collectAsState(). User events flow up through function calls that trigger data updates, completing the cycle.
The pattern is simple once you trace it:
- Data flows down through reactive streams
- Events flow up through function calls
- Each layer has a single responsibility
- StateFlow in ViewModel is the bridge
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Compose Architecture Guide
- 👨💻 State in Compose
- 👨💻 Kotlin Flow Documentation
- 👨💻 Now in Android Sample
- 👨💻 Reddit: Understanding MVVM Clean Architecture
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments