100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Kotlin

Unidirectional Data Flow

A pattern where state flows down from a single source of truth to the UI, and events flow up to trigger state changes, keeping Compose UIs predictable and testable.

Architecture & ViewModelIntermediate9 min readJul 8, 2026
Analogies

Unidirectional Data Flow

Unidirectional Data Flow (UDF) is an architectural principle where data has a single, well-defined path through an application: state flows down from a source of truth to the composables that render it, and events (clicks, text input, swipes) flow up from those composables to the layer that owns and can mutate the state. In Jetpack Compose this maps naturally onto state hoisting — a stateless composable receives a value and a lambda callback as parameters instead of owning mutable state itself. UDF is not a Compose-specific invention; it is the same idea behind Redux, Elm, and MVI, but Compose's recomposition model makes it especially natural because composables are just functions of state.

🏏

Cricket analogy: UDF's state-down, events-up flow is like the captain (source of truth) setting the field, and fielders (composables) signaling back up when they spot a chance, rather than fielders repositioning themselves independently.

The practical benefit is predictability. When state can only be modified in one place (typically a ViewModel), you never have to hunt through a dozen composables to figure out who last mutated a value. Bugs where two widgets silently disagree about the current state become structurally impossible, because there is exactly one source of truth and every consumer reads from it. This also makes UI logic trivially unit-testable: you can construct a state object, pass it into a composable in a test, and assert on the emitted semantics tree without touching a real UI toolkit.

🏏

Cricket analogy: Predictability from a single mutation point is like only the official scorer being allowed to update the scorecard, so no one has to ask three different commentators who last changed the run total.

The UDF Loop

A typical UDF cycle in an Android app looks like this: (1) the ViewModel exposes an immutable UI state via StateFlow; (2) the Composable collects that state with collectAsStateWithLifecycle and renders it; (3) the user interacts, firing an event such as onClick or onValueChange; (4) the composable invokes a lambda passed down from the caller, which forwards the event to the ViewModel; (5) the ViewModel processes the event, computes a new state, and emits it; (6) Compose observes the state change and recomposes only the affected composables. The loop is closed but the data itself only ever travels in one direction per hop — down as state, up as events — never both ways through the same channel.

🏏

Cricket analogy: The UDF cycle from StateFlow to recomposition is like the scorer updating the official total (state), the scoreboard operator rendering it, a boundary being hit (event), that event reaching the scorer, who recalculates and republishes the new total.

kotlin
data class TodoUiState(
    val items: List<TodoItem> = emptyList(),
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

class TodoViewModel(private val repository: TodoRepository) : ViewModel() {
    private val _uiState = MutableStateFlow(TodoUiState(isLoading = true))
    val uiState: StateFlow<TodoUiState> = _uiState.asStateFlow()

    init {
        viewModelScope.launch {
            repository.observeTodos().collect { todos ->
                _uiState.update { it.copy(items = todos, isLoading = false) }
            }
        }
    }

    fun onTodoChecked(id: String, checked: Boolean) {
        viewModelScope.launch { repository.setChecked(id, checked) }
    }
}

@Composable
fun TodoScreen(viewModel: TodoViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    TodoList(
        items = uiState.items,
        onCheckedChange = viewModel::onTodoChecked
    )
}

@Composable
fun TodoList(items: List<TodoItem>, onCheckedChange: (String, Boolean) -> Unit) {
    LazyColumn {
        items(items, key = { it.id }) { todo ->
            TodoRow(todo = todo, onCheckedChange = { checked -> onCheckedChange(todo.id, checked) })
        }
    }
}

UDF vs. Two-Way Binding

Traditional View-based Android and some UI frameworks favor two-way data binding, where a widget can both read and silently write back to a shared model (e.g. an EditText bound directly to a database field). This is convenient for small forms but scales poorly: it becomes hard to reason about who changed what and when, and it invites subtle bugs where a UI update triggers a data update which triggers another UI update. Compose deliberately steers developers away from two-way binding in favor of explicit, one-directional flows — even a simple text field uses value (down) and onValueChange (up) rather than mutating a shared reference directly.

🏏

Cricket analogy: Two-way binding is like a scorer and an assistant both being allowed to edit the same scorecard directly, risking one overwriting the other's entry, while UDF is like only the scorer updating it and the assistant just reading it.

Think of UDF like a river system: rain (events) falls into tributaries and flows into one central reservoir (the ViewModel's state). Every town downstream (composable) draws its water from that single reservoir, never directly from each other's pipes. This is exactly why two composables observing the same StateFlow are guaranteed to render consistent data.

A common anti-pattern is letting a child composable hold its own remember { mutableStateOf(...) } for data that also lives in the ViewModel. Now there are two sources of truth that can drift apart. If a value needs to survive configuration changes, be shared across composables, or be tested independently of the UI, it belongs in hoisted state or the ViewModel — not duplicated locally.

  • UDF means state flows down and events flow up through exactly one channel each.
  • Compose implements UDF via state hoisting: composables take state as parameters and events as lambdas.
  • A single source of truth (usually a ViewModel-owned StateFlow) prevents inconsistent UI state.
  • UDF makes composables easier to unit test because they are pure functions of their inputs.
  • Avoid duplicating ViewModel-owned data into local remembered state — that reintroduces two sources of truth.
  • UDF is a general architectural pattern shared with Redux/Elm/MVI, not something unique to Compose.

Practice what you learned

Was this page helpful?

Topics covered

#Kotlin#AndroidWithJetpackComposeStudyNotes#MobileDevelopment#UnidirectionalDataFlow#Unidirectional#Data#Flow#UDF#StudyNotes#SkillVeris