Sobes.tech
Middle+

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:

  1. Side Effects (SingleLiveEvent / Channels):

    A special mechanism for sending events from ViewModel to View. SingleLiveEvent (in older projects or libraries like androidx.lifecycle:lifecycle-livedata-ktx), or Channel from 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
        }
    }
    
  2. Single Use Events in State:

    A less preferred option, but sometimes used in simple cases. Add a flag or field in the UiState itself that indicates a one-time event. After processing the event in the View, the View sends a new Intent to 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.

How to handle events that do not need to be saved in… - sobes.tech