DEV Community

Cover image for Android MVI: O que é e como funciona essa arquitetura?
Emanuel Galvão
Emanuel Galvão

Posted on

Android MVI: O que é e como funciona essa arquitetura?

Trabalhando com desenvolvimento para a plataforma Android provavelmente você já deve ter ficado na dúvida sobre como uma arquitetura específica de software funciona ou ainda quais são os pontos positivos e negativos de usar uma arquitetura em um projeto ou módulo novo.

Nesse artigo vou tentar responder algumas dessas dúvidas relacionadas à arquitetura MVI (Model-View-Intent), que vem sendo uma arquitetura cada vez mais adotada e comum dentro do mundo de desenvolvimento mobile, principalmente dentro do Android. Além disso, vou trazer um comparativo com outra arquitetura muito comum dentro da plataforma que é o MVVM (Model-View-ViewModel) de forma a te ajudar a entender a aplicação de cada uma delas.

O que é?

O MVI consiste em uma arquitetura baseada em intenções e em um fluxo de dados unidirecional, essas intenções podem ser entendidas como ações que podem ser realizadas pelo usuário durante o uso de uma funcionalidade ou aplicação. Tudo o que realiza a alteração de estado da aplicação, qualquer clique ou interação do usuário com a aplicação pode ser representado por uma intenção (Intent).

Para entender em uma visão geral como funciona a arquitetura, precisamos primeiro saber o que a compõe. Como normalmente ocorre, cada letra da sigla que dá nome à arquitetura representa uma parte que integra o conjunto da obra.

No MVI temos essa sigla representando 3 peças principais:

  • Model: Consiste em um estado único que é responsável por representar uma tela ou funcionalidade, esse estado possui todas as informações necessárias para a construção das Views.
  • View: É a camada de visualização das informações, o que de fato o usuário visualiza e interage de forma a resolver o problema proposto pela aplicação. Aqui temos toda a parte de Activity, Fragment, Composables e qualquer outra interface de interação.
  • Intent: Essa é a parte em que se baseia toda a arquitetura, que basicamente são as intenções/ações que o usuário pode realizar durante o uso de uma funcionalidade ou aplicação. Um ponto importante sobre esse conceito, ele não tem nenhuma relação com o Intent usado no Android para abertura de Activities e/ou comunicação com outros aplicativos.

Figura 1 - Comunicação entre as camadas


Figura 1 - Comunicação entre as camadas

Na Figura 1 exibida acima, é possível ver de forma macro como funciona a interação entre as camadas nessa arquitetura. De forma simples, a View é a tela que é exibida para o usuário, ele interage com essa tela e realiza ações, que são as Intents, essas intenções demonstram a necessidade de uma manipulação de dados para atender a solicitação. A Model é a responsável por manter um estado único e representar por completo a tela que é exibida ao usuário.

Como essa arquitetura é organizada no Android?

Acredito que a melhor forma de explicar sobre a organização dessa arquitetura dentro de um projeto Android é através de um exemplo prático.

Vamos pensar na construção de uma tela inicial de um aplicativo esboçada pela Figura 2 abaixo. Nela podemos ver que a tela possui uma seção de banners, uma lista de produtos e uma bottomsheet para o usuário realizar o login.

Figura 2 - Esboço da tela inicial


Figura 2 - Esboço da tela inicial

Agora com essa tela em mente podemos começar a organizar o nosso estado geral da tela, as interações que o usuário pode realizar e quais são os eventos únicos que podem ser exibidos pela interface.

Para o estado geral vejo a necessidade de termos as informações abaixo:

  • Saber se a tela está carregando ou se já terminou de carregar.
  • Receber a lista de banners que devem ser exibidos.
  • Receber a lista de produtos que devem ser exibidos.
  • Saber se precisamos exibir a bottomsheet de login ou não.

Agora pensando nas ações que a tela pode ter e que o usuário pode realizar, temos:

  • Ação de carregar as informações da tela, seja no primeiro carregamento ou em uma atualização da tela.
  • Ação de clicar para realizar o login usando email e senha.
  • Ação de clicar para ver um produto específico.

Por fim, temos os possíveis eventos únicos que a tela pode receber que são os eventos que exigem algum processamento que é responsabilidade da View, no nosso caso foquei em apenas um:

  • Navegar para a tela de detalhes de um produto específico.

Com esses comportamentos definidos, podemos representar isso no código através das 3 entidades abaixo:

data class UiState(
    val isLoading: Boolean = false,
    val banners: List<Banner> = listOf(),
    val products: List<Product> = listOf(),
    val showLogin: Boolean = true
)

sealed interface Intent {
    data object LoadData: Intent
    data class Login(
        val email: String,
        val password: String
    ): Intent
    data class GoToProduct(
        val productId: String
    ): Intent
}

sealed interface Effect {
    data class NavigateToProduct(
        val productId: String
    ): Effect
}
Enter fullscreen mode Exit fullscreen mode

Além dessas entidades estabelecidas acima, precisamos definir mais uma peça central para nossa aplicação, que é o nosso Reducer. O Reducer é o componente responsável por receber o estado atual e também os dados necessários para uma mudança de estado específica, com isso em mãos o Reducer consegue fazer a atualização dos parâmetros e devolver o novo estado que será propagado para a nossa View. Para os nossos comportamentos definidos anteriormente, precisamos que o Reducer saiba tratar os seguintes cenários:

  • Controle da exibição do loading enquanto realizamos algum processamento.
  • Recebimento dos dados que devem ser exibidos na tela e atualização das informações no estado.
  • Recebimento do sucesso no login e tratamento para esconder a bottomsheet.

Com isso definido podemos criar o HomeReducer, sendo uma classe que vai possuir um único método de reduce que sempre recebe o estado atual e a ação que ele deve processar, podemos representar ele pela seguinte classe:

class HomeReducer {
    sealed interface Action {
        data object DisplayLoading: Action
        data class DataLoaded(
            val banners: List<Banner>,
            val products: List<Product>
        ): Action
        data object LoginSuccess: Action
    }

    fun reduce(state: UiState, action: Action): UiState {
        return when (action) {
            Action.DisplayLoading -> state.copy(isLoading = true)
            is Action.DataLoaded -> state.copy(
                isLoading = false,
                banners = action.banners,
                products = action.products
            )
            Action.LoginSuccess -> state.copy(
                isLoading = false,
                showLogin = false
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Com essas informações definidas, podemos detalhar melhor todo o fluxo de dados em cada uma das interações. Na Figura 3 abaixo temos a separação da comunicação entre a View, ViewModel e a camada de dados, nela podemos ver por completo a interação de todos os fluxos que descrevi anteriormente.

Começando a explicação pela ViewModel, fica claro que ela expõe para a Activity um UiState através de um StateFlow e um Effect através de um Channel, além de um método handle que recebe sempre uma Intent. Do lado da Activity vemos que possuímos uma Screen feita em Jetpack Compose e também temos dois Observers que acompanham as atualizações do UiState e do Effect fornecidos pela ViewModel. Ainda temos a camada de dados com os UseCases, Repositories e comunicação com uma API remota.

Figura 3 - Fluxo completo de uma tela de Home na arquitetura MVI


Figura 3 - Fluxo completo de uma tela de Home na arquitetura MVI

Explicando de fato os fluxos possíveis dessa tela mostrados na imagem e como eles funcionam:

1. Fluxo inicial: Ao entrar na tela precisamos realizar as buscas dos dados que devem ser exibidos, isso será realizado usando uma API remota. Para esse objetivo temos os passos demonstrados no Diagrama de Sequência 1 abaixo:

Diagrama de Sequência 1 - Fluxo inicial de carregamento da home


Diagrama de Sequência 1 - Fluxo inicial de carregamento da home

2. Fluxo de login: Com a bottomsheet sendo exibida damos a opção do usuário realizar o login digitando o email e senha. Ao clicar no botão de “Entrar” segue-se a sequência demonstrada pelo Diagrama de Sequência 2 abaixo:

Diagrama de Sequência 2 - Fluxo de sucesso no login


Diagrama de Sequência 2 - Fluxo de sucesso no login

3. Fluxo de seleção de produto: Tendo a lista de produtos sendo mostrada na tela existe a possibilidade do usuário realizar a seleção de um deles de forma a visualizar mais informações sobre ele. Ao realizar essa ação temos o fluxo mostrado no Diagrama de Sequência 3 abaixo:

Diagrama de Sequência 3 - Fluxo de navegação para detalhes de produto


Diagrama de Sequência 3 - Fluxo de navegação para detalhes de produto

Observação: Para deixar mais simples a explicação e exibição na imagem não foram mostrados os fluxos de erros, porém eles basicamente envolveriam uma atualização no estado ou ainda uma ocorrência única de exibição de mensagens explicando as falhas.

Com isso concluímos a explicação completa de como funcionam todos os fluxos que tínhamos definido para essa tela na análise inicial.

Quais são os benefícios que essa arquitetura traz?

Dentre os principais benefícios que a arquitetura MVI oferece, podemos focar nos quatro descritos abaixo:

  • Mais clareza na compreensão dos fluxos disponíveis de uma tela/funcionalidade, de forma que é simples entender quais são os comportamentos e estados que uma tela/funcionalidade pode oferecer.
  • Facilidade em realizar a criação de testes dos fluxos, como temos a clareza do fluxo e qual é o estado final esperado é fácil criar um teste que irá validar se o resultado obtido condiz com o desejado ou se temos alguma falha no fluxo.
  • Compatibilidade e integração com o Jetpack Compose, pois como foi demonstrado anteriormente temos o estado completo da tela e as intenções disponíveis, com isso é simples criar os comportamentos que a interface deve fornecer.
  • Separação de estados da tela e de eventos únicos, de forma a separar o que são mudanças persistentes nas informações da tela e o que são ocorrências únicas, como navegações ou exibições de mensagens de erro.

Quando não utilizar a arquitetura MVI?

Além dos pontos positivos, temos que pensar em quais cenários não seria relevante usar essa arquitetura. De forma simples não é interessante quando estamos trabalhando nos seguintes casos:

  • Para aplicações muito simples é pouco vantajoso o uso da arquitetura MVI devido à quantidade de classes necessárias para orquestração dos fluxos como foi mostrado antes. Por isso, para cenários básicos de funcionalidades pouco complexas talvez seja interessante analisar outras opções de arquiteturas.
  • Times com pouco conhecimento nos conceitos de programação reativa, pois exige uma curva de aprendizado maior para conseguir entender e aplicar essa arquitetura no projeto.
  • Aplicações que exigem entrega rápida ou são apenas protótipos ou ainda tem interfaces visuais muito simples e com poucos estados.

O que diferencia a arquitetura MVI e a MVVM?

Para fazer um comparativo e elucidar um pouco mais sobre as principais diferenças entre a forma de uso de cada uma dessas arquiteturas organizei a tabela abaixo trazendo as diferenças entre ambas em alguns tópicos específicos.

MVI MVVM
Estado Um único objeto de estado que representa a tela por completo Pode oferecer múltiplos objetos de estado para diversos elementos da tela
Fluxo de Dados Fluxo de dados unidirecional, de forma a ficar claro todo o curso das informações Possibilidade de fluxo bidirecional, com a ViewModel expondo estados e até mesmo callbacks para uso pela View
Comunicação View⭤ViewModel A ViewModel expõe um único método de acesso para a View, recebendo sempre uma intenção e permitindo que a ViewModel tenha total controle do fluxo Normalmente a View tem acesso a vários métodos da ViewModel, de forma a View poder chamar vários deles, dificultando o controle do fluxo apenas pela ViewModel
Erros São controlados de forma direta através do único objeto de estado ou ainda por um objeto de Effect/SideEffect responsável por eventos únicos da tela Tratamento de erros pode ocorrer misturado entre os vários objetos de estado expostos pela ViewModel
Atualização de Dados Possui um Reducer, responsável por utilizar os dados e fazer a atualização correta do estado Usa a própria ViewModel para fazer o controle e atualização dos estados

Conclusão

Espero que eu tenha conseguido deixar um pouco mais claro como funciona a arquitetura MVI no Android, de forma a você conseguir compreender e aplicar em suas aplicações no futuro. Acredito que pude explicar como funciona usando um fluxo real e que também tenha apresentado as vantagens e desvantagens em utilizá-la. Se restar alguma dúvida e/ou sugestão de melhoria, deixo o campo dos comentários aberto para discutirmos.

Fontes

[1] E. P. Beltré, “Yes, That’s MVI: The Pattern’s Full History, Misconceptions, and Modern Android Form,” Medium, May 29, 2025. https://proandroiddev.com/yes-that-is-mvi-674f810ca4fe (acessado em 04/10/2026).

[2] S. O. Akhtar, “Mastering MVI in Android: A Complete Guide with Sample Code,” Medium, Feb. 18, 2026. https://blog.stackademic.com/mastering-mvi-in-android-a-complete-guide-with-sample-code-70d312a9a140 (acessado em 04/10/2026).

[3] J. SANTOS, “Introdução ao padrão MVI no desenvolvimento Android,” DIO, Nov. 24, 2023. https://www.dio.me/articles/introducao-ao-padrao-mvi-no-desenvolvimento-android (acessado em 04/10/2026).

Top comments (0)