Unidirectional data flow and UiState

Architecture and dependency injection Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Order the steps

Order one trip around the unidirectional loop after a pull-to-refresh.

  • The UI calls viewModel.onRefresh()
  • The repository fetches and saves to the database
  • The UI recomposes from the new state
  • The ViewModel sets isRefreshing = true in UiState
  • The database flow emits; the ViewModel builds a new UiState

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A D B E C

  1. The UI calls viewModel.onRefresh() Events go up as function calls.
  2. The ViewModel sets isRefreshing = true in UiState Only the state holder changes state.
  3. The repository fetches and saves to the database The data layer does the work.
  4. The database flow emits; the ViewModel builds a new UiState New data arrives as state.
  5. The UI recomposes from the new state The UI only renders.

30-second answer

In unidirectional data flow, state flows down from a state holder to the UI and events flow up from the UI to the state holder, which updates the state. The ViewModel exposes one immutable UiState (often a data class or sealed interface) through a StateFlow, and the UI only renders it. There's one owner and one direction, so the screen is predictable and easy to test.

Practise saying it · 35 s. No recording, just a timer

Explain unidirectional data flow.

Say your answer out loud, then check which of these points you covered:

  • State down, events up
  • One immutable UiState
  • Events as functions
  • Easy to test and preview

Deep dive

A data class with isLoading, items and error fields suits screens that show several things at once; a sealed interface suits mutually exclusive states.

Expose functions for events (onRefresh()), not mutable state.

Build UiState with combine from several repository flows, then stateIn.

Rendering is a pure function of state, so screenshot tests and previews can cover every case.

A UiState and its ViewModel Compile-checked · Kotlin 2.4.21
data class FeedUiState(
    val items: List<Article> = emptyList(),
    val isRefreshing: Boolean = false,
    val error: String? = null,
)

class FeedViewModel(private val load: suspend () -> List<Article>) : ViewModel() {
    private val _state = MutableStateFlow(FeedUiState())
    val state: StateFlow<FeedUiState> = _state.asStateFlow()

    fun onRefresh() = viewModelScope.launch {
        _state.update { it.copy(isRefreshing = true, error = null) }
        try {
            val items = load()
            _state.update { it.copy(isRefreshing = false, items = items) }
        } catch (e: IOException) {
            _state.update { it.copy(isRefreshing = false, error = "You're offline") }
        }
    }
}

Common mistakes

  • Exposing several separate mutable flows that can get out of sync.
  • Letting the UI write into the ViewModel's state directly.
  • Using Booleans that allow impossible combinations.

Senior take & follow-ups

Draw the loop: state down, events up. Then show one UiState type.

Follow-up questions

Think of your answer first, then open the hint.

  1. Data class or sealed interface for UiState?

    Show hint

    Sealed for mutually exclusive states, data class when parts coexist.

  2. How do you test it?

    Show hint

    Call an event function and assert the next UiState.

Similar on iOS: Which property wrapper for which data? SwiftUI's Observation pushes state to views the same way, without an explicit StateFlow.

Suggest a fix if something here is wrong or out of date.