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
- Uso de Sealed Interfaces: Modelagem exaustiva de todas as intenções e efeitos colaterais permitidos na funcionalidade.
- Channel BUFFERED para Side-Effects: Garante a entrega confiável de eventos únicos para a UI sem perda de mensagens.
- Método
updatedo 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
-
Slides da Aula
-
Quiz de Fixação
-
Exercícios Práticos
-
Desafio de Projeto