On this page
In a bidirectional design, a text field updates a variable, the variable updates a label, the label's listener updates another variable... and nobody can say which value is "correct" when they disagree. Unidirectional data flow (UDF) removes that ambiguity with one rule: state flows down, events flow up.
The rule
- State is owned by a single source of truth and flows down to consumers, which can only read it.
- Events (user actions, system callbacks) flow up to the owner, which decides how state changes.
- The owner produces a new immutable state, which flows down again.
UDF end to end
// Data layer owns the truth
class PostRepository @Inject constructor(private val dao: PostDao, private val api: PostApi) {
fun observePost(id: String): Flow<Post> = dao.observe(id).map { it.toDomain() }
suspend fun setLiked(id: String, liked: Boolean) {
dao.setLiked(id, liked) // optimistic local update
runCatching { api.setLiked(id, liked) }.onFailure { dao.setLiked(id, !liked); throw it }
}
}
// ViewModel turns data into UI state and handles events
class PostViewModel(private val repo: PostRepository, postId: String) : ViewModel() {
private val id = postId
val uiState = repo.observePost(id)
.map { PostUiState(title = it.title, liked = it.liked, likeCount = it.likeCount) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), PostUiState())
fun onLikeClick() = viewModelScope.launch {
runCatching { repo.setLiked(id, !uiState.value.liked) }
}
}
// UI renders state and emits events; it never flips `liked` itself
@Composable
fun LikeButton(liked: Boolean, count: Int, onClick: () -> Unit) {
TextButton(onClick = onClick) {
Icon(if (liked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, contentDescription = null)
Text(" $count")
}
}Even the optimistic update goes through the database, so every screen showing this post agrees.
Benefits
- Consistency: one owner per piece of data means no conflicting copies.
- Predictability: state changes happen in known places, so bugs are easier to trace.
- Testability: feed events in, assert state out.
- Survives configuration changes, because state lives in ViewModels and repositories rather than Views.
Common UDF violations
| Violation | Symptom | Fix |
|---|---|---|
UI mutates shared state directly (public MutableStateFlow) | State changes from unexpected places | Expose read-only StateFlow; mutate via ViewModel functions |
| Local copy of ViewModel state in a composable | UI and ViewModel disagree | Read directly from state; hoist instead of copying |
| Two ViewModels both caching the same data | Screens show different values | Move the truth to the data layer and observe it |
| Child components calling repositories | Hidden data flows, hard tests | Events up to the screen's ViewModel |
UDF at different scales
Component level
State hoisting: value plus onValueChange. See State Hoisting.
Screen level
ViewModel exposes UiState, and the screen calls functions. See ViewModel and StateFlow.
App level
The database as source of truth, with network sync writing into it. See Offline-First.
Frequently asked questions
Is UDF the same as MVI?
MVI is a strict form of UDF that formalises events as intents and changes as a reducer. MVVM with a single immutable state is also UDF.
Doesn't UDF add latency for text input?
Not noticeably when state updates are synchronous on the main thread, as with MutableStateFlow.update or Compose state. Avoid asynchronous round-trips for every keystroke.
Where do one-off events fit?
Model them as state that the UI consumes and clears, so they still follow the unidirectional loop.
Conclusion
UDF is a simple discipline: one owner per piece of state, state down as immutable values, events up as function calls. Apply it at component, screen and app level and your Android code becomes predictable, consistent and testable.
Next step: scale the codebase physically with Multi-Module Architecture in Android.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Architecture makes the most sense when you apply it to a real project and see why each boundary exists. Learn how maintainable Android applications are designed by building one.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.