|
| 1 | +# Handling one-off events |
| 2 | + |
| 3 | +One-time events such as displaying a snackbar or navigating to a different destination that are typically passed from the ViewModel to the UI are implemented using Compose State. No SharedFlows or Channels involved. |
| 4 | + |
| 5 | +For example, after deleting a habit, ViewModel decides to throw an event that tells the UI to navigate to a different screen. For that it updates the respective `StateFlow` variable. The UI observes this event in a `LaunchedEffect` block, which executes the action once the state gets changed. |
| 6 | + |
| 7 | +## Benefits |
| 8 | + |
| 9 | +- state can't be lost in any scenario ([read more](https://medium.com/androiddevelopers/viewmodel-one-off-event-antipatterns-16a1da869b95)); |
| 10 | +- easy to use not only in `ViewModel`s, but also in plain state holder classes; |
| 11 | +- ensures immediate response to new events (such as when a snackbar needs to be updated immediately upon a new event, instead of waiting for the previous message to disappear). |
| 12 | + |
| 13 | +## Limitations |
| 14 | + |
| 15 | +One-off events are handled with state but state is not really suitable for this purpose it keeps the value even after the event is finished. So this requires us to manually reset the state after it’s consumed. |
| 16 | + |
| 17 | +Othewise, some unexpected behavior may happen. Continuing the initial example, if the user tries to navigate back to the screen where the habit was deleted, it will not actually navigate because the state in that screen will immediately tell the program to navigate back. |
| 18 | + |
| 19 | +That’s why resetting the state is unified throughout the project with the help of extensions in [`UiEvent.kt`](https://github.com/DanielRendox/RoutineTracker/blob/main/core/ui/src/main/java/com/rendox/routinetracker/core/ui/helpers/UiEvent.kt) |
| 20 | + |
| 21 | +## How to use |
| 22 | + |
| 23 | +So all events should be objects of `UiEvent` interface and should typcially be nullable so that when the event is fired off, it could be reset to null. This interface has a function called `onConsumed` which should be overriden when the event is fired off. You can also pass data using `val data`. |
| 24 | + |
| 25 | +All events should be observed in the UI using `ObserveUiEvent` composable. This composable automatically calls `event.onConsumed()` once the action is finished. |
| 26 | + |
| 27 | +Back to our example with deleting a habit. We would implement navigation as follows: |
| 28 | + |
| 29 | +```kotlin |
| 30 | +class RoutineDetailsScreenViewModel: ViewModel() { |
| 31 | + private val _navigateBackEvent: MutableStateFlow<UiEvent<Any>?> = MutableStateFlow(null) |
| 32 | + val navigateBackEvent = _navigateBackEvent.asStateFlow() |
| 33 | + |
| 34 | + fun onDeleteHabit() = viewModelScope.launch { |
| 35 | + /* the logic of deleting a habit */ |
| 36 | + |
| 37 | + _navigateBackEvent.update { |
| 38 | + object : UiEvent<Any> { |
| 39 | + override val data: Any = Unit |
| 40 | + override fun onConsumed() { |
| 41 | + _navigateBackEvent.update { null } |
| 42 | + } |
| 43 | + } |
| 44 | + } |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +@Composable |
| 49 | +fun RoutineDetailsScreen( |
| 50 | + viewModel: RoutineDetailsScreenViewModel = koinViewModel(), |
| 51 | + navigateBack: () -> Unit, |
| 52 | +) { |
| 53 | + val navigateBackEvent by viewModel.navigateBackEvent.collectAsStateWithLifecycle() |
| 54 | + ObserveUiEvent(navigateBackEvent) { |
| 55 | + navigateBack() |
| 56 | + } |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +So here we have a `StateFlow` that is initially set to `null`. When the user wants to delete the habit, `navigateBackEvent` gets a value of the `UiEvent` object. There is an implementation of `onConsumed` defined that resets our `StateFlow` back to null. |
| 61 | + |
| 62 | +Then we observe this event using `ObserveUiEvent` in the compose code. After the we’ve navigated back, the `navigateBackEvent` will be reset to null automatically. |
| 63 | + |
| 64 | +> Now, in practice, it is rarely necessary to reset the state back and we could get away without doing it. However, we must follow this pattern for all one-off events to keep consistency inside the codebase and avoid unexpected behavior. |
0 commit comments