Capítulo 10: Arquitetura MVVM & MVI com AndroidX ViewModel e Unidirectional Data Flow

Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose


🗺️ Mapa Conceitual do Ciclo MVI (Model-View-Intent) e UDF

flowchart TD
    subgraph VIEW["Camada de Apresentação (View / Compose)"]
        UI["Tela @Composable (Renderiza UiState)"]
        USER_ACTION["Ação do Usuário (Toque em Botão / Digitação)"]
        UI --> USER_ACTION
    end

    subgraph INTENT["Intenção do Usuário (UiIntent / UserEvent)"]
        EVENT["CarregarDados / Filtrar / SubmeterFormulario"]
        USER_ACTION -->|"Dispara Evento"| EVENT
    end

    subgraph VIEWMODEL["Camada de Estado (AndroidX ViewModel)"]
        VM["ViewModel (Gerencia viewModelScope e Sobrevive à Rotação)"]
        EVENT -->|"Processa Intent"| VM
        REDUCE["Função Redutora / Atualização Funcional de Estado"]
        VM --> REDUCE
        STATE_FLOW["StateFlow<UiState> (Fonte Única da Verdade)"]
        REDUCE --> STATE_FLOW
        SIDE_EFFECT["Channel / SharedFlow<UiEffect> (Snackbars / Navegação)"]
        VM --> SIDE_EFFECT
    end

    STATE_FLOW -->|"Coleta Reativa (collectAsStateWithLifecycle)"| UI
    SIDE_EFFECT -->|"Eventos Únicos (One-shot)"| UI

    style UI fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style USER_ACTION fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style EVENT fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style VM fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style STATE_FLOW fill:#b3e5fc,stroke:#0288d1,stroke-width:2px
    style SIDE_EFFECT fill:#fce4ec,stroke:#e91e63,stroke-width:2px

🏛️ 1. A Convergência entre MVVM e MVI no Ecossistema Declarativo

A transição das interfaces Android para o paradigma funcional do Jetpack Compose aproximou drasticamente os padrões MVVM (Model-View-ViewModel) e MVI (Model-View-Intent). Em ambos os modelos modernos, a regra primordial é o Unidirectional Data Flow (UDF):

  1. Fonte Única da Verdade (SSOT): A tela nunca detém estados de negócio fragmentados. O ViewModel expõe um único stream reativo de estado imutável (StateFlow<UiState>). Toda mutação resulta na emissão de uma nova instância imutável do estado, eliminando estados intermediários inconsistentes ou telas parcialmente renderizadas.
  2. Ciclo de Vida do ViewModel: O AndroidX ViewModel é retido em memória pelo framework Android através do ViewModelStoreOwner durante mudanças de configuração (como giros de tela ou troca de idioma). Suas tarefas assíncronas são encapsuladas no viewModelScope, que cancela automaticamente todas as coroutines filhas quando o ViewModel é descartado permanentemente no onCleared().
  3. MVI e Intenções Explícitas: No padrão MVI, as interações do usuário deixam de ser simples métodos dispersos (vm.onSalvar(), vm.onDeletar()) e passam a ser modeladas como tipos selados explícitos (UiIntent). Isso viabiliza testes unitários determinísticos, auditoria de eventos e reprodução de cenários complexos de bug.

📊 2. Comparativo Estrutural: MVVM Tradicional vs MVI Moderno

Dimensão Arquitetural MVVM Tradicional (LiveData / Múltiplos Fluxos) MVI Moderno (UDF com StateFlow & Sealed Types)
Exposição de Estado Vários LiveDatas avulsos (isLoading, userName, hasError) Um único objeto atômico StateFlow<TelaUiState>
Consistência Visual Risco de “Glitch de UI” (ex: loading falso com dados antigos) Impossível (o estado transiciona atomicamente)
Interações do Usuário Métodos imperativos avulsos no ViewModel Stream ou despachador de eventos selados UiIntent
Efeitos de Disparo Único Booleans que exigiam reset manual (handled = true) Channel<UiEffect> consumido como evento pontual
Testabilidade Unitária Múltiplas asserções em observers independentes Asserção direta na linha do tempo das emissões de UiState

💻 3. Laboratório Prático: ViewModel MVI com StateFlow e UiEffect

O exemplo a seguir demonstra a implementação de um ViewModel de produção para gestão de tarefas, aplicando UDF e separação estrita entre Estado Persistente (UiState) e Efeitos Colaterais Pontuais (UiEffect):

package br.com.portal.mobile.android.cap10

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

// 1. Modelagem do Estado Atômico Imutável
data class TarefasUiState(
    val tarefas: List<String> = emptyList(),
    val estaCarregando: Boolean = false,
    val filtroAtivo: Boolean = false
)

// 2. Intenções Explícitas do Usuário (MVI Intents)
sealed interface TarefasIntent {
    data object CarregarTarefasIniciais : TarefasIntent
    data class AdicionarTarefa(val descricao: String) : TarefasIntent
    data class AlternarFiltro(val apenasUrgentes: Boolean) : TarefasIntent
}

// 3. Efeitos Colaterais Únicos (One-shot Effects para SnackBar / Navegação)
sealed interface TarefasEffect {
    data class ExibirAlerta(val mensagem: String) : TarefasEffect
}

// 4. ViewModel Orquestrador com UDF
class TarefasViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(TarefasUiState(estaCarregando = true))
    val uiState: StateFlow<TarefasUiState> = _uiState.asStateFlow()

    private val _effect = Channel<TarefasEffect>(Channel.BUFFERED)
    val effect = _effect.receiveAsFlow()

    init {
        despacharIntent(TarefasIntent.CarregarTarefasIniciais)
    }

    fun despacharIntent(intent: TarefasIntent) {
        when (intent) {
            is TarefasIntent.CarregarTarefasIniciais -> carregarTarefas()
            is TarefasIntent.AdicionarTarefa -> adicionarTarefa(intent.descricao)
            is TarefasIntent.AlternarFiltro -> alternarFiltro(intent.apenasUrgentes)
        }
    }

    private fun carregarTarefas() {
        viewModelScope.launch {
            // Simulação de delay de banco local
            _uiState.update {
                it.copy(
                    tarefas = listOf("Refatorar Módulo Room", "Escrever Testes Compose"),
                    estaCarregando = false
                )
            }
        }
    }

    private fun adicionarTarefa(descricao: String) {
        if (descricao.isBlank()) {
            viewModelScope.launch {
                _effect.send(TarefasEffect.ExibirAlerta("A descrição da tarefa não pode ser vazia."))
            }
            return
        }

        _uiState.update { estadoAtual ->
            estadoAtual.copy(tarefas = estadoAtual.tarefas + descricao)
        }

        viewModelScope.launch {
            _effect.send(TarefasEffect.ExibirAlerta("Tarefa adicionada com sucesso!"))
        }
    }

    private fun alternarFiltro(apenasUrgentes: Boolean) {
        _uiState.update { it.copy(filtroAtivo = apenasUrgentes) }
    }
}

⚠️ 4. Armadilhas Comuns em Arquitetura Android

  1. Guardar Referências a Context ou Views no ViewModel: Armazenar Context, Activity, View ou elementos da composição dentro de um ViewModel causa vazamentos severos de memória (Memory Leaks), impedindo que a Activity seja recolhida pelo Garbage Collector quando destruída.
  2. Coletar Flows no Compose sem collectAsStateWithLifecycle: Coletar um Flow usando apenas collectAsState() mantém a coleta ativa mesmo quando o app entra em segundo plano (background), drenando a bateria e o plano de dados do usuário. Utilize sempre collectAsStateWithLifecycle(), que pausa o stream quando a tela não está visível.
  3. Emitir Eventos Únicos via StateFlow: Usar StateFlow para eventos que só devem acontecer uma única vez (como exibir um Snackbar ou navegar de tela) faz com que o evento seja re-executado sempre que a tela for recomposta ou girada. Prefira sempre Channel ou SharedFlow.