Sobes.tech
Middle+

Jak obsługiwać w architekturze MVI zdarzenia, które nie muszą być zapisywane?

sobes.tech AI

Odpowiedź od AI

Aby obsługiwać unikalne zdarzenia (np. wyświetlenie toasta, nawigacja) w MVI, które nie powinny przetrwać ponownego tworzenia widoku ani być przechowywane w stanie, stosuje się różne podejścia:

  1. Efekty uboczne (SingleLiveEvent / Channels):

    Specjalny mechanizm do wysyłania zdarzeń z ViewModel do View. SingleLiveEvent (w starszych projektach lub bibliotekach typu androidx.lifecycle:lifecycle-livedata-ktx), lub Channel z Flow (w nowoczesnych projektach). Zapewniają, że zdarzenie zostanie obsłużone tylko raz.

    // ViewModel z Flow i 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() {
            // Logika biznesowa...
            viewModelScope.launch {
                _sideEffect.send(SideEffect.ShowToast("Operacja zakończona sukcesem!"))
            }
        }
    }
    
    sealed class SideEffect {
        data class ShowToast(val message: String) : SideEffect()
        object NavigateNext : SideEffect()
    }
    
    // W widoku (Fragment/Activity) obserwujemy 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) {
            // Implementacja wyświetlania toast
        }
    
        private fun navigateToNextScreen() {
            // Implementacja nawigacji
        }
    }
    
  2. Zdarzenia jednorazowe w stanie:

    Mniej zalecane, ale czasami używane w prostych przypadkach. Dodaje się flagę lub pole w samym stanie UiState, które wskazuje na obecność zdarzenia jednorazowego. Po obsłudze zdarzenia, widok wysyła nowy Intent do ViewModel, aby zresetować tę flagę/pole w stanie. Może to skomplikować logikę i potencjalnie prowadzić do ponownego wywołania przy nieprawidłowym obsługiwaniu lub ponownym tworzeniu widoku.

    // Przykład stanu z flagą jednorazową
    data class MyUiState(
        val data: List<Item> = emptyList(),
        val isLoading: Boolean = false,
        val showSuccessToast: Boolean = false // Flaga zdarzenia jednorazowego
    )
    
    // W ViewModel po udanej operacji
    fun processSuccess() {
        _uiState.update { it.copy(showSuccessToast = true) }
    }
    
    // W widoku, gdy showSuccessToast = true
    if (state.showSuccessToast) {
        showToast("Sukces!")
        // Ważne: wysłać Intent, aby zresetować flagę
        viewModel.onToastShown()
    }
    
    // W ViewModel, aby zresetować flagę
    fun onToastShown() {
        _uiState.update { it.copy(showSuccessToast = false) }
    }
    

Najlepszym zalecanym podejściem w nowoczesnym Androidzie z Flow i MVI jest użycie Efektów ubocznych przez Channel. To czyste, bezpieczne i przewidywalne rozwiązanie dla zdarzeń jednorazowych.