Pular para conteúdo

Aula 18 - Arquitetura MVVM/MVI com Kotlin Coroutines e Flow ⚡

Objetivo Pedagógico

Objetivo: Estruturar a camada de apresentação e lógica de negócios utilizando o padrão Model-View-Intent (MVI) com Kotlin Coroutines, StateFlow para retenção de estado e SharedFlow para eventos efêmeros únicos (SingleLiveEvent).


📑 1. Fundamentos Teóricos & Análise Técnica

À medida que a complexidade dos aplicativos Android se expande, arquiteturas baseadas em múltiplos estados mutáveis desarticulados (como múltiplos MutableLiveData) geram inconsistências graves de tela e bugs de sincronismo.

O padrão Model-View-Intent (MVI) e o ecossistema de corrotinas estabelecem um fluxo determinístico: 1. Os Pilares do MVI: - Intent (Intenção): Uma sealed class ou interface que representa qualquer ação intencional disparada pelo usuário ou pelo sistema (ex: CarregarDados, AtualizarFiltro, SubmeterFormulario). - State (Estado): Uma única data class imutável que encapsula toda a representação da tela (ex: loading, dados de sucesso, mensagens de erro). A tela é estritamente uma função pura do estado: $ ext{UI} = f( ext{State})$. - Side-Effect (Efeito Colateral): Eventos transitórios que não devem ser persistidos no estado da UI e devem ser executados exatamente uma vez (ex: navegar para outra tela, exibir Snackbar de erro temporário). 2. StateFlow vs. SharedFlow: - StateFlow: Fluxo observável conflated que retém sempre o último valor. Ideal para o UiState, pois sobrevive a recomposições e mudanças de configuração (rotação de tela). - SharedFlow: Fluxo com buffer configurável sem retenção permanente de valor. Perfeito para despachar efeitos colaterais de disparo único (Navigation Events), evitando que uma mensagem de erro seja reexibida ao girar o aparelho. 3. Escopos Estruturados e Concorrência Segura: - As corrotinas operam dentro do viewModelScope, sendo canceladas automaticamente quando a tela é destruída pelo ciclo de vida do Android, impedindo vazamentos de memória.

📐 Arquitetura Conceitual & Diagrama de Fluxo

graph LR
    UI["Compose UI"] -->|Dispara Intent (ex: Refresh)| VM["ViewModel"]
    VM -->|Executa no Dispatcher de IO| Repo["Repository (Corrotinas)"]
    Repo -->|Retorna Resultado| VM
    VM -->|Atualiza StateFlow| State["UiState Imutável"]
    VM -->|Emite SharedFlow| Effect["SideEffect (Navegar / Snackbar)"]
    State -->|Coletado com Lifecycle| UI
    Effect -->|Coletado em LaunchedEffect| UI
    style UI fill:#e1f5fe,stroke:#01579b
    style VM fill:#fff3e0,stroke:#e65100
    style Repo fill:#e8f5e9,stroke:#2e7d32
    style State fill:#f3e5f5,stroke:#7b1fa2
    style Effect fill:#ffebee,stroke:#c62828

🔍 Pilares e Diretrizes Técnicas

Nesta unidade, aprofundamos os seguintes conceitos fundamentais: - Concorrência Estruturada (Structured Concurrency): Garantia de que corrotinas filhas pertençam a um Job pai e sejam canceladas conjuntamente sem deixar tarefas zumbis. - Tratamento Atômico de Estado (update): Uso de MutableStateFlow.update { ... } para mutações thread-safe atômicas sem corridas de concorrência. - Coleta Segura com collectAsStateWithLifecycle: Pausa a coleta de dados quando o aplicativo vai para segundo plano, economizando bateria e ciclos de CPU. - Isolamento de Dispatchers: Execução de chamadas de rede e banco estritamente em Dispatchers.IO preservando a thread principal (Dispatchers.Main).


🛠️ 2. Implementação Prática em Arquitetura Reativa Android, Coroutines e StateFlow

Abaixo está a implementação técnica de referência, estruturada com padrões de engenharia de software e foco em robustez:

// OrdersViewModel.kt (Arquitetura MVI com StateFlow e SharedFlow)
package com.portal.android.feature.orders

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

sealed interface OrderIntent {
    object LoadOrders : OrderIntent
    data class CancelOrder(val orderId: String) : OrderIntent
}

data class OrderUiState(
    val isLoading: Boolean = false,
    val orders: List<String> = emptyList(),
    val errorMessage: String? = null
)

sealed interface OrderSideEffect {
    data class ShowToast(val message: String) : OrderSideEffect
    data class NavigateToDetails(val orderId: String) : OrderSideEffect
}

class OrdersViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(OrderUiState())
    val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()

    private val _sideEffect = Channel<OrderSideEffect>(Channel.BUFFERED)
    val sideEffect = _sideEffect.receiveAsFlow()

    fun handleIntent(intent: OrderIntent) {
        when (intent) {
            is OrderIntent.LoadOrders -> loadOrdersData()
            is OrderIntent.CancelOrder -> cancelOrderById(intent.orderId)
        }
    }

    private fun loadOrdersData() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            // Simulação de chamada assíncrona segura
            kotlinx.coroutines.delay(800)
            _uiState.update { it.copy(isLoading = false, orders = listOf("Pedido #101", "Pedido #102")) }
        }
    }

    private fun cancelOrderById(orderId: String) {
        viewModelScope.launch {
            _sideEffect.send(OrderSideEffect.ShowToast("Pedido $orderId cancelado!"))
        }
    }
}

💡 Análise Passo a Passo do Código

  1. Uso de Sealed Interfaces: Modelagem exaustiva de todas as intenções e efeitos colaterais permitidos na funcionalidade.
  2. Channel BUFFERED para Side-Effects: Garante a entrega confiável de eventos únicos para a UI sem perda de mensagens.
  3. Método update do StateFlow: Garante mutação atômica do estado sem condições de corrida em concorrência multi-thread.

🎯 3. Próximos Passos & Sequência Didática