-
Notifications
You must be signed in to change notification settings - Fork 0
State, Events & Side Effects
Andrés Pedraza Míguez edited this page Jan 25, 2026
·
1 revision
This project follows a Unidirectional Data Flow (UDF) pattern—specifically a variation of MVI (Model-View-Intent)—to ensure a clean separation of concerns, predictability, and testability.
We strictly distinguish between State (what the UI shows), Events (what the user does), and Side Effects (one-off occurrences).
- Definition: Represents the persistent "snapshot" of the screen at any given moment.
-
Implementation:
StateFlow<UiState>. -
Characteristics:
- Sticky: It always has a value. When a view subscribes, it immediately receives the latest state.
- Persistent: It survives configuration changes (like screen rotations).
- Usage: Use this for data that must remain visible if the user rotates the phone or navigates away and back quickly.
- Examples: Loading indicators, lists of data (Expenses, Groups), input field text, validation errors (e.g., "Title cannot be empty").
- Definition: Represents the user's intent or inputs. These are the "Inputs" to the ViewModel.
-
Implementation: A single public method
fun onEvent(event: UiEvent)that accepts a sealed interface/class. -
Characteristics:
-
Decoupled: The UI does not call specific helper methods (e.g.,
submitData()). Instead, it notifies the ViewModel that an event occurred (e.g.,SubmitButtonClicked). - Traceable: Makes debugging easier as we can log every action flowing into the ViewModel.
-
Decoupled: The UI does not call specific helper methods (e.g.,
-
Examples:
TitleChanged,SubmitPressed,DeleteClicked.
- Definition: Represents ephemeral, "fire-and-forget" occurrences that should happen exactly once. These are the "One-shot Outputs".
-
Implementation:
SharedFlow<UiAction>(configured for no replay) orChannel. -
Characteristics:
- Ephemeral: These events are not part of the state. They happen and disappear.
- Non-Sticky: If the UI is not observing when the effect is emitted, it might be dropped (intentional for UI feedback).
- Lifecycle Aware: They should not re-trigger upon screen rotation.
- Examples: Showing a Toast/Snackbar, navigating to another screen, vibration feedback, playing a sound.
The most common mistake is putting "One-shot" events into the State.
-
Bad Practice: Adding a
showErrorToast: Booleanto the UiState.-
Problem: If the user rotates the screen, the Activity recreates, observes the State again, sees
showErrorToast = true, and shows the Toast again. You then have to manually reset the boolean to false.
-
Problem: If the user rotates the screen, the Activity recreates, observes the State again, sees
-
Good Practice: Emitting a
ShowErrorToastaction viaSharedFlow.-
Benefit: The Toast is consumed once. If the screen rotates, the
SharedFlowdoes not replay the old event, preventing duplicate feedback.
-
Benefit: The Toast is consumed once. If the screen rotates, the
Using onEvent enforces a strict API surface. The View acts purely as a dumb renderer that forwards user intents. It prevents the View from knowing how the ViewModel processes logic, allowing for easier refactoring of the ViewModel without breaking the UI.
// State: Persistent data
data class AddExpenseUiState(
val title: String = "",
val amount: String = "",
val isLoading: Boolean = false,
val titleError: String? = null // Validation error is State (visual)
)
// Event: Inputs from the User
sealed interface AddExpenseUiEvent {
data class TitleChanged(val newTitle: String) : AddExpenseUiEvent
object SubmitPressed : AddExpenseUiEvent
}
// Action: One-shot Side Effects
sealed interface AddExpenseUiAction {
data class ShowSnackbar(val message: String) : AddExpenseUiAction
object NavigateBack : AddExpenseUiAction
}