MVI — Entendendo o Padrão Model-View-Intent em Apps Móveis

Autor: IT Sectr Publicado: 2026-02-16 Tempo de leitura: 10 min

MVI (Model-View-Intent) é um padrão arquitetural reativo baseado em fluxo de dados unidirecional e estado imutável. Ao contrário do MVVM, onde uma ViewModel pode ter vários StateFlows, o MVI define um estado único (State), intenções imutáveis (Intent) e uma função redutora pura (Reducer). O MVI garante a previsibilidade do estado da tela em qualquer momento. O padrão foi popularizado na comunidade Android pelas bibliotecas Mosby e Orbit. Saiba mais em MVIKotlin de Arkadii Ivanov.

Principais Conclusões

  • MVI — Model (estado), View (exibição), Intent (intenção do usuário) — um ciclo reativo
  • Unidirectional data flow — os dados fluem em uma direção: Intent → Reducer → State → View
  • Immutable State — o estado da tela é um objeto imutável, recriado a cada alteração
  • Reducer — uma função pura que recebe o estado atual e um Intent, retornando um novo estado
  • Side effects — efeitos colaterais (rede, BD) são tratados separadamente do Reducer, via Middleware

O que é MVI: a essência do padrão Model-View-Intent

MVI (Model-View-Intent) é um padrão arquitetural reativo construído sobre os princípios de Redux e Cycle.js. Model é o estado imutável da tela, Intent é uma intenção do usuário ou sistema, View se inscreve no estado e envia Intents. Os dados fluem em um ciclo: o usuário interage com a View → a View cria um Intent → o Intent é processado pelo Reducer → o Reducer cria um novo estado → a View recebe o novo estado e renderiza novamente.

A principal diferença entre MVI e MVVM é a Fonte Única de Verdade. No MVVM, uma ViewModel pode ter vários LiveData/StateFlow (userState, loadingState, errorState), o que leva à inconsistência: loading=true e user=null simultaneamente. No MVI, existe exatamente uma classe/interface sealed State que descreve todo o estado da tela. Em qualquer momento, o estado da tela é determinado de forma única — é impossível ter loading=true quando os dados já estão carregados. Na IT Sectr, aplicamos MVI para telas com lógica complexa — formulários de pedido, registros de várias etapas, telas financeiras — onde a previsibilidade do estado é crítica.

ComponenteFunção no MVIExemplo
IntentIntenção do usuário ou sistemaLoadUser, Refresh, SubmitForm
StateEstado imutável da telasealed class UserState
ReducerFunção pura: State + Intent → Statefun reduce(state, intent) -> state
MiddlewareTratamento de efeitos colateraisRequisição de rede, escrita em BD

O ciclo MVI consiste em cinco etapas: 1) A View envia um Intent (ex.: LoadUser(42)); 2) O Middleware (EffectHandler) executa um efeito colateral — uma requisição de rede; 3) O resultado é retornado como um novo Intent ao sistema; 4) O Reducer recebe o estado atual e o Intent, cria um novo estado; 5) A View recebe o novo estado e renderiza novamente. Cada etapa é previsível e testável isoladamente.

MVI no Android: Intent, Reducer, State em Kotlin

MVI no Android é implementado usando classes sealed para Intent e State, uma ViewModel com lógica MVI e Jetpack Compose para renderização reativa. A ViewModel recebe Intent da View, delega efeitos colaterais ao Middleware, executa o Reducer e publica o novo estado via StateFlow. O Jetpack Compose renderiza novamente a UI quando o estado muda — ideal para o ciclo MVI.

kotlin
// Intent — intenções do usuário
sealed interface UserIntent {
    data class LoadUser(val userId: Int) : UserIntent
    data object Refresh : UserIntent
}

// State — estado único da tela
sealed interface UserState {
    data object Idle : UserState
    data object Loading : UserState
    data class Success(val user: User) : UserState
    data class Error(val message: String) : UserState
}

// Reducer — função pura
object UserReducer {
    fun reduce(state: UserState, intent: UserIntent): UserState = when (intent) {
        is UserIntent.LoadUser -> UserState.Loading
        is UserIntent.Refresh -> UserState.Loading
    }
}

// ViewModel com MVI
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    private val _state = MutableStateFlow<UserState>(UserState.Idle)
    val state: StateFlow<UserState> = _state.asStateFlow()

    fun process(intent: UserIntent) {
        val newState = UserReducer.reduce(_state.value, intent)
        _state.value = newState
        when (intent) {
            is UserIntent.LoadUser -> loadUser(intent.userId)
            is UserIntent.Refresh -> loadUser(/* ID anterior */)
        }
    }

    private fun loadUser(userId: Int) {
        viewModelScope.launch {
            repository.getUser(userId)
                .onSuccess { user ->
                    _state.value = UserState.Success(user)
                }
                .onFailure { e ->
                    _state.value = UserState.Error(e.message ?: "Error")
                }
        }
    }
}

// View envia Intent
fun UserScreen(viewModel: UserViewModel) {
    val state by viewModel.state.collectAsState()
    LaunchedEffect(Unit) { viewModel.process(UserIntent.LoadUser(42)) }
    when (state) {
        is UserState.Loading -> CircularProgressIndicator()
        is UserState.Success -> UserCard((state as UserState.Success).user)
        is UserState.Error -> ErrorView((state as UserState.Error).message)
        is UserState.Idle -> Text("Press to load")
    }
}

Middleware e efeitos colaterais — no MVI, um Reducer puro não pode fazer requisições de rede. O Middleware (também EffectHandler ou Bootstrapper) processa o Intent, executa o efeito colateral e emite um novo Intent de volta ao ciclo. As bibliotecas Orbit MVI e MVIKotlin fornecem suporte embutido a Middleware com efeitos testáveis. Sem Middleware, o MVI degenera em MVVM com estrutura adicional de Intent e State.

MVIKotlin de Arkadii Ivanov é a biblioteca MVI mais popular para Kotlin Multiplatform. Suporta Android, iOS, web e JVM. Fornece componentes: Store (ViewModel), Bootstrapper (efeitos iniciais), Reducer, Middleware. Em outubro de 2025, a biblioteca tem 2.5K estrelas no GitHub e é usada em projetos comerciais, incluindo aplicativos de grandes bancos russos. Na IT Sectr, usamos MVIKotlin para projetos multiplataforma KMP com lógica de negócios compartilhada.

MVI no iOS: fluxo unidirecional em Swift

MVI no iOS é implementado sem Combine-ViewModel, através do ciclo Intent → State. A View envia um Intent via closure, o Reducer é uma função pura, e State é uma struct com campos imutáveis. O SwiftUI renderiza novamente a View quando State muda, o que se encaixa perfeitamente no ciclo MVI sem propriedades @Published adicionais. MVI no iOS é especialmente popular entre desenvolvedores SwiftUI que migraram do Redux (JavaScript).

swift
// State — estrutura imutável
struct UserState: Equatable {
    var user: User?
    var isLoading = false
    var errorMessage: String?
}

// Intent — enum com intenções
enum UserIntent {
    case loadUser(id: Int)
    case refresh
    case userLoaded(User)
    case loadFailed(Error)
}

// Reducer — função pura
func userReducer(state: UserState, intent: UserIntent) -> UserState {
    var newState = state
    switch intent {
    case .loadUser, .refresh:
        newState.isLoading = true
        newState.errorMessage = nil
    case .userLoaded(let user):
        newState.isLoading = false
        newState.user = user
    case .loadFailed(let error):
        newState.isLoading = false
        newState.errorMessage = error.localizedDescription
    }
    return newState
}

// Store — possui o estado e gerencia os efeitos
final class UserStore: ObservableObject {
    @Published private(set) var state = UserState()
    private let service: UserService

    init(service: UserService) {
        self.service = service
    }

    func dispatch(_ intent: UserIntent) {
        // 1. Reducer atualiza o estado
        state = userReducer(state: state, intent: intent)
        // 2. Side effects (se necessário)
        switch intent {
        case .loadUser(let id), .refresh:
            service.fetchUser(id: id) { [weak self] result in
                switch result {
                case .success(let user):
                    self?.dispatch(.userLoaded(user))
                case .failure(let error):
                    self?.dispatch(.loadFailed(error))
                }
            }
        default: break
        }
    }
}

TCA (The Composable Architecture) é a implementação MVI mais popular para iOS da Point-Free, construída sobre SwiftUI e Combine. TCA fornece Store, Reducer, Effect e Environment. Em outubro de 2025, suas estrelas no GitHub ultrapassam 13K — é o padrão de facto para MVI no iOS. TCA é usado nos aplicativos Starbucks, Airbnb (parcialmente) e muitos projetos independentes. Ao contrário do MVI personalizado, TCA resolve testes, navegação e efeitos colaterais prontamente.

MVI vs MVVM no iOS — TCA/MVI fornece previsibilidade de estado mas requer mais código boilerplate (Reducer, State, Action). MVVM com @Published é mais simples para telas básicas. Na IT Sectr, usamos MVVM para 80% das telas e MVI (TCA) para 20% complexas — transações financeiras, formulários de várias etapas, interfaces drag-and-drop, onde um erro de estado pode custar dinheiro ao usuário.

Comparação MVI vs MVVM: quando escolher MVI

MVI e MVVM resolvem o mesmo problema — organizar a camada de Apresentação — mas com diferentes abordagens ao gerenciamento de estado. MVVM permite múltiplas fontes reativas (LiveData, @Published), o que pode levar à inconsistência. MVI garante exatamente um estado a qualquer momento, tornando-o mais rigoroso e previsível, mas aumenta a quantidade de código.

CritérioMVVMMVI
EstadoMúltiplos LiveData/StateFlowClasse sealed State única
Fluxo de dadosBidirecional (View → ViewModel, LiveData → View)Unidirecional (Intent → Reducer → State → View)
Efeitos colateraisDiretamente na ViewModelAtravés de Middleware/EffectHandler
TestesTestes unitários da ViewModelTestes unitários do Reducer + Middleware
Código boilerplateMínimoReducer + State + Intent + Middleware

Quando escolher MVI — telas onde o estado deve ser estritamente determinístico: operações financeiras, carrinho de compras, formulários de várias etapas com validação em cada passo. Nestes cenários, o custo de um erro de estado (por exemplo, mostrar o total do carrinho faltando um item devido a uma condição de corrida entre dois LiveData) supera o custo do código adicional. No MVVM, você confia na disciplina da equipe; no MVI, você confia na arquitetura.

Quando MVVM é suficiente — 80% das telas padrão: lista de usuários, perfil, configurações, feed de notícias. Aqui, um estado único é excessivo, e a estrutura adicional do MVI retardará o desenvolvimento. Na IT Sectr, a regra é: se uma tela tem 3+ estados possíveis com transições (carregamento → dados → erro → tentar novamente → carregamento → dados) — use MVI. Se uma tela tem 1-2 operações assíncronas — use MVVM.

Melhores práticas de MVI e erros comuns

Sealed State — melhor prática em MVI. O estado é definido como uma classe/interface sealed com variantes: Loading, Success(data), Error(message). Isso garante que a View não termine em um estado inconsistente — não é possível exibir dados quando loading=true porque Loading e Success são classes diferentes. Todos os dados relacionados ao estado residem dentro da variante sealed: Success contém o usuário, Error contém a mensagem de erro.

O Reducer deve permanecer uma função pura — sem chamadas de API, BD ou SharedPreferences. Uma função pura recebe State e Intent e retorna State. Efeitos colaterais (rede, BD, navegação, toasts) são tratados no Middleware ou no Store.dispatch após chamar o Reducer. Se o Reducer for poluído com efeitos colaterais, o MVI perde testabilidade e previsibilidade — você obtém MVVM com estrutura adicional sem benefícios.

Erros comuns — declarar State como data class com campos nullable em vez de sealed class: data class UserState(val user: User?, val isLoading: Boolean, val error: String?). Isso é equivalente a MVVM, não a MVI — a View deve verificar combinações de campos para validade. Na abordagem sealed, combinações inválidas (isLoading=true e user!=null) são impossíveis no nível de tipos. O segundo erro é colocar lógica de negócios no Intent (Intent.LoadUserBeforeXHours) em vez de criar Intents de comando simples (Intent.LoadUser) e colocar a lógica de negócios no Middleware.

Perguntas Frequentes

Qual é a principal diferença entre MVI e MVVM?

MVI usa uma única classe sealed State imutável e fluxo de dados unidirecional através de um Reducer. MVVM permite múltiplos LiveData/StateFlow com vinculação bidirecional. MVI garante consistência de estado no nível de tipos — é impossível ter loading=true e user=null simultaneamente. MVVM depende da disciplina do desenvolvedor.

Quais bibliotecas MVI existem para Android?

Principais: MVIKotlin (Arkadii Ivanov, 2.5K estrelas, Kotlin Multiplatform), Orbit MVI (BabyJ, 1.3K estrelas), Mobius (Spotify, Kotlin/Java). MVIKotlin é a mais popular para Kotlin, Orbit é a mais fácil de aprender. Todas as três suportam Reducer e Middleware testáveis. Para Jetpack Compose, você pode escrever MVI simples sem biblioteca usando sealed State + Reducer.

É necessária uma biblioteca separada para MVI?

Não — Intent sealed + State sealed + ViewModel + StateFlow fornece MVI funcional sem dependências. As bibliotecas (MVIKotlin, Orbit, TCA) adicionam Middleware, teste de efeitos colaterais e integração com DI. Para projetos simples, o peso da biblioteca é injustificado. Para projetos complexos com 20+ telas, a biblioteca se paga com tratamento estruturado de efeitos.

MVI é adequado para iOS ou é apenas um padrão Android?

MVI funciona muito bem para iOS através do TCA (The Composable Architecture) — a arquitetura mais popular da comunidade SwiftUI. TCA é essencialmente MVI + Redux + Combine. No iOS, você pode implementar MVI sem TCA usando ObservableObject e uma função redutora pura. SwiftUI com State imutável se encaixa perfeitamente no ciclo MVI.

Como testar MVI?

O Reducer é testado com testes unitários como função pura: defina um State inicial, envie um Intent, verifique o State resultante. O Middleware é testado com um repositório mock: verifique se getUser foi chamado após LoadUser. Teste de ViewModel: envie um Intent, verifique o StateFlow. MVI é mais fácil de testar que MVVM porque o Reducer é uma função pura sem dependências ocultas.

Resumo

  • MVI (Model-View-Intent) — um padrão reativo com fluxo unidirecional e estado único
  • Sealed State — garante consistência no nível de tipos, eliminando combinações inválidas
  • Reducer — função pura State + Intent → State, testável sem objetos mock
  • Middleware — uma camada separada para efeitos colaterais (rede, BD, navegação)
  • MVI vs MVVM — MVI é mais rigoroso e previsível, MVVM é mais simples e rápido
  • Android — MVIKotlin ou Orbit para telas complexas; MVVM para simples
  • iOS — TCA (The Composable Architecture) é o padrão MVI no SwiftUI

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também