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-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.
| Componente | Função no MVI | Exemplo |
|---|---|---|
| Intent | Intenção do usuário ou sistema | LoadUser, Refresh, SubmitForm |
| State | Estado imutável da tela | sealed class UserState |
| Reducer | Função pura: State + Intent → State | fun reduce(state, intent) -> state |
| Middleware | Tratamento de efeitos colaterais | Requisiçã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 é 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.
// 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 é 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).
// 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.
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ério | MVVM | MVI |
|---|---|---|
| Estado | Múltiplos LiveData/StateFlow | Classe sealed State única |
| Fluxo de dados | Bidirecional (View → ViewModel, LiveData → View) | Unidirecional (Intent → Reducer → State → View) |
| Efeitos colaterais | Diretamente na ViewModel | Através de Middleware/EffectHandler |
| Testes | Testes unitários da ViewModel | Testes unitários do Reducer + Middleware |
| Código boilerplate | Mínimo | Reducer + 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.
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
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.
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.
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 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.
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
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.
Leia também