How to handle events that do not need to be saved in the MVI architecture?
sobes.tech AI
Answer from AI
For handling one-time events (such as displaying a toast, navigation) in MVI, which should not survive View recreation or be stored in state, various approaches are used:
-
Side Effects (SingleLiveEvent / Channels):
A special mechanism for sending events from ViewModel to View.
SingleLiveEvent(in older projects or libraries likeandroidx.lifecycle:lifecycle-livedata-ktx), orChannelfrom Flow (in modern projects). They guarantee that the event will be consumed only once.// ViewModel with Flow and Channel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch class MyViewModel : ViewModel() { private val _sideEffect = Channel<SideEffect>(Channel.BUFFERED) val sideEffect = _sideEffect.receiveAsFlow() fun doSomething() { // Business logic... viewModelScope.launch { _sideEffect.send(SideEffect.ShowToast("Operation completed successfully!")) } } } sealed class SideEffect { data class ShowToast(val message: String) : SideEffect() object NavigateNext : SideEffect() } // In View (Fragment/Activity), observe SideEffect import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch class MyFragment : Fragment() { private val viewModel: MyViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { viewModel.sideEffect.collect { effect -> when (effect) { is SideEffect.ShowToast -> showToast(effect.message) SideEffect.NavigateNext -> navigateToNextScreen() } } } } private fun showToast(message: String) { // Implementation of showing toast } private fun navigateToNextScreen() { // Implementation of navigation } } -
Single Use Events in State:
A less preferred option, but sometimes used in simple cases. Add a flag or field in the
UiStateitself that indicates a one-time event. After processing the event in the View, the View sends a newIntentto the ViewModel to reset this flag/field in the state. This can complicate logic and potentially lead to repeated triggers on improper handling or View recreation.// Example of State with a one-time flag data class MyUiState( val data: List<Item> = emptyList(), val isLoading: Boolean = false, val showSuccessToast: Boolean = false // Flag for one-time event ) // In ViewModel, upon successful operation fun processSuccess() { _uiState.update { it.copy(showSuccessToast = true) } } // In View, when handling showSuccessToast = true if (state.showSuccessToast) { showToast("Success!") // Important: send an Intent to reset the flag viewModel.onToastShown() } // In ViewModel, to reset the flag fun onToastShown() { _uiState.update { it.copy(showSuccessToast = false) } }
The most recommended approach in modern Android with Flow and MVI is to use Side Effects via Channel. It is a clean, safe, and predictable solution for one-time events.