UI State Management
Relevant source files
The following files were used as context for generating this wiki page:- app/src/main/java/com/vocabri/ui/screens/dictionary/viewmodel/DictionaryViewModel.kt
- app/src/main/java/com/vocabri/ui/screens/dictionary/viewmodel/DictionaryState.kt
- app/src/main/java/com/vocabri/ui/main/viewmodel/MainViewModel.kt
- app/src/main/java/com/vocabri/ui/screens/addword/viewmodel/AddWordViewModel.kt
- app/src/main/java/com/vocabri/ui/screens/addword/viewmodel/AddWordState.kt
- app/src/main/java/com/vocabri/ui/screens/addword/viewmodel/AddWordEvent.kt
- app/src/main/java/com/vocabri/ui/screens/dictionarydetails/viewmodel/DictionaryDetailsViewModel.kt
- app/src/main/java/com/vocabri/ui/screens/dictionarydetails/viewmodel/DictionaryDetailsState.kt
- app/src/main/java/com/vocabri/ui/screens/dictionarydetails/viewmodel/DictionaryDetailsEvent.kt
The Vocabri application employs a modern, reactive UI state management architecture based on the Unidirectional Data Flow (UDF) pattern. This approach is well-suited for the Jetpack Compose UI toolkit and leverages Android Architecture Components like ViewModel alongside Kotlin Coroutines and StateFlow. The core idea is to have a single source of truth for the UI state, which flows in one direction from the ViewModel to the UI. User interactions are captured as Events and sent to the ViewModel to be processed, which may result in a new state being emitted.
This architecture promotes a clear separation of concerns, making the UI layer a simple function of its state. It enhances testability by isolating business logic in ViewModels and Use Cases, and improves predictability by ensuring that state changes are explicit and traceable. Data is exposed to the UI via a StateFlow, which Compose can observe to automatically trigger recomposition when the state changes.
Core Architectural Components
The state management pattern is built on three primary components for each screen or complex UI element: ViewModel, State, and Event.
- ViewModel: An Android
ViewModelthat holds and processes application data, survives configuration changes, and exposes the UI state. It is the only component that can modify the state. - State: A data class or sealed interface that represents everything the UI needs to render itself at any given moment. It is immutable.
- Event: A sealed interface representing user actions or other triggers from the UI that the
ViewModelneeds to process.
The relationship between these components is illustrated below.
This diagram shows that the UI observes the State from the ViewModel and sends Events to it. The ViewModel processes these events and may produce a new State.
Sources: app/src/main/java/com/vocabri/ui/screens/dictionary/viewmodel/DictionaryViewModel.kt:43, app/src/main/java/com/vocabri/ui/screens/addword/viewmodel/AddWordViewModel.kt:51, app/src/main/java/com/vocabri/ui/screens/dictionarydetails/viewmodel/DictionaryDetailsViewModel.kt:66
State Representation
UI state is modeled using Kotlin's sealed interface. This allows for a well-defined and exhaustive set of possible states for a screen, such as Loading, Empty, Error, or a state containing loaded data. This approach makes it easy to handle all possible UI scenarios in the Composable functions.
Common State Patterns
Most screens follow a similar state machine pattern, transitioning between loading, data-loaded, empty, and error states.
| State Class | File | Description |
|---|---|---|
DictionaryState | DictionaryState.kt | Represents the state for the main dictionary screen, which shows word groups. |
DictionaryDetailsState | DictionaryDetailsState.kt | Represents the state for the screen showing a list of words for a specific part ofspeech. |
AddWordState | AddWordState.kt | Represents the state for the "Add Word" form, including a complex Editing state and a final Saved state. |
Sources: app/src/main/java/com/vocabri/ui/screens/dictionary/viewmodel/DictionaryState.kt, app/src/main/java/com/vocabri/ui/screens/dictionarydetails/viewmodel/DictionaryDetailsState.kt, app/src/main/java/com/vocabri/ui/screens/addword/viewmodel/AddWordState.kt
The following diagram shows the typical state transitions for a data-loading screen like the Dictionary.
This flow is implemented in DictionaryViewModel where it starts in Loading, and based on the result of observeWordGroupsUseCase, it moves to GroupsLoaded, Empty, or Error.
Sources: app/src/main/java/com/vocabri/ui/screens/dictionary/viewmodel/DictionaryViewModel.kt:65-87
Example: DictionaryDetailsState
The DictionaryDetailsState is a sealed interface that defines the possible states for the word list screen.
sealed interface DictionaryDetailsState {
val titleId: Int
data class WordsLoaded(@param:StringRes override val titleId: Int, val words: List<WordUiModel>) :
DictionaryDetailsState
data class Empty(@param:StringRes override val titleId: Int) : DictionaryDetailsState
data class Loading(@param:StringRes override val titleId: Int) : DictionaryDetailsState
data class Error(@param:StringRes override val titleId: Int, val message: String) : DictionaryDetailsState
}
Sources: app/src/main/java/com/vocabri/ui/screens/dictionarydetails/viewmodel/DictionaryDetailsState.kt:26-37
Data Flow and Event Handling
The UDF pattern is strictly followed. User actions in the UI are translated into Event objects and passed to the ViewModel's handleEvent method. The ViewModel then processes the event, which may involve calling a use case, performing calculations, and finally updating its internal MutableStateFlow. This update triggers a recomposition of the UI with the new state.
Sequence of Operations
The following sequence diagram illustrates the process of deleting a word from the DictionaryDetailsScreen.
This diagram shows how a UI event triggers a cascade of calls through the ViewModel and Use Case to the data layer. The ViewModel optimistically updates the UI by removing the word from the list in the current state before the background deletion is complete.
Sources: app/src/main/java/com/vocabri/ui/screens/dictionarydetails/viewmodel/DictionaryDetailsViewModel.kt:68-69, app/src/main/java/com/vocabri/ui/screens/dictionarydetails/viewmodel/DictionaryDetailsViewModel.kt:143-148, app/src/main/java/com/vocabri/ui/screens/dictionarydetails/viewmodel/DictionaryDetailsViewModel.kt:151-164
ViewModel Implementations
DictionaryViewModel
This ViewModel is responsible for the main dictionary screen. It observes word groups from the ObserveWordGroupsUseCase.
- State:
DictionaryState(Loading,Empty,GroupsLoaded,Error). - Initialization: It immediately calls
observeWordGroups()in itsinitblock to start loading data. - Data Observation: It uses a
Flowfrom the use case, which is collected within theviewModelScope. The flow is started with aLoadingstate. - State Transformation: It transforms the
List<WordGroup>from the domain layer intoList<WordGroupUiModel>for the UI, including resolving string resources. - Error Handling: It catches exceptions from the flow and sets an
Errorstate. It also provides aretry()function to re-trigger the observation.
Sources: app/src/main/java/com/vocabri/ui/screens/dictionary/viewmodel/DictionaryViewModel.kt:49-53, app/src/main/java/com/vocabri/ui/screens/dictionary/viewmodel/DictionaryViewModel.kt:58-91
AddWordViewModel
This is a more complex ViewModel that manages the state of a form with multiple fields and dynamic lists (translations, examples).
- State:
AddWordState(Editing,Saved,Error). TheEditingstate is a large data class holding all form fields. - Event Handling: It has a large
whenblock inhandleEventto process dozens of different events, such asUpdateText,AddTranslation,UpdatePartOfSpeech, andSaveWord. - State Updates: Each event handler function typically calls
_state.updateon the currentEditingstate, returning acopy()of the state with the modified fields. - Business Logic: It contains logic to enable/disable the save button, validate inputs before saving, and construct the final
Worddomain model object from the form state. - Saving: The
saveWord()function assembles aWordobject and callsaddWordsUseCase. It handles success by transitioning to theSavedstate and handles errors by transitioning to theErrorstate.
Sources: app/src/main/java/com/vocabri/ui/screens/addword/viewmodel/AddWordViewModel.kt:51-90, app/src/main/java/com/vocabri/ui/screens/addword/viewmodel/AddWordViewModel.kt:313-372
MainViewModel
The MainViewModel is simpler and manages the state for the main activity, primarily handling global actions.
- State:
MainState, which currently only contains the list of bottom navigation routes. - Event Handling: It handles a
MainEvent.OnPlusButtonLongClickevent, which is a debug feature to generate and save a random word. - Asynchronous Work: It launches a coroutine in
viewModelScopeto call thegenerateRandomWordUseCaseandaddWordsUseCaseon an I/O dispatcher.
Sources: app/src/main/java/com/vocabri/ui/main/viewmodel/MainViewModel.kt:47-51, app/src/main/java/com/vocabri/ui/main/viewmodel/MainViewModel.kt:53-65
Summary
The UI state management in Vocabri is a robust and scalable implementation of the Unidirectional Data Flow pattern. By using ViewModel, StateFlow, and a system of State and Event sealed interfaces, the architecture achieves a clean separation between UI and business logic. This results in a codebase that is predictable, easier to debug, and highly testable. The use of Kotlin Coroutines and Flow ensures that data loading and processing are handled efficiently without blocking the main thread, providing a smooth user experience.