Ir para o conteúdo

No dia a dia de nossos projetos, é quase automático focar diretamente na solução dos problemas que encontramos no caminho. Preocupamo-nos em estruturar o código, corrigir bugs e entregar as funcionalidades solicitadas pelos nossos clientes. Isso é normal, pois temos prazos para cumprir as demandas e não podemos ficar refletindo demais sobre a “arquitetura limpa” ou nos “design patterns”. Mesmo assim, é importante que, de vez em quando, reservemos um momento para refletir sobre o motivo pelo qual fazemos o que fazemos. Questionar essas questões nos faz sair da zona de conforto e ir além do código.

Entender o que acontece nos bastidores é o que nos impulsiona para patamares mais elevados como desenvolvedores, pois é compreendendo o funcionamento das coisas que podemos resolver os problemas mais complexos dos projetos e tomar as melhores decisões. Saber os padrões de arquitetura, no dia a dia, talvez não seja tão útil assim, mas no momento em que você se deparar com um projeto que utiliza um padrão completamente diferente do que está acostumado, terá uma segurança muito maior para lidar com os desafios e propor soluções de melhoria. E a ideia é essa: não precisamos saber detalhadamente como implementar algo, seja aquela arquitetura ou um framework lançado recentemente, mas devemos compreender o conceito por trás daquilo.

A proposta deste artigo é ampliar nossos horizontes no que diz respeito às arquiteturas Android. Para entender isso, conhecer a história desses modelos pode ajudar a compreender verdadeiramente o motivo de cada escolha e nos fornecer ferramentas para lidar melhor com os problemas encontrados em nossos projetos.

O que é um padrão de projeto?

Um padrão de projeto refere-se a uma solução arquitetônica ou uma abordagem de design que resolve problemas recorrentes no desenvolvimento. Esses padrões ajudam a estruturar o código de maneira organizada, promovem a reutilização, facilitam a manutenção e aprimoram a escalabilidade do aplicativo. Foi nesse contexto que surgiram os padrões que conhecemos hoje, como o MVC, MVP, e assim por diante. Esses padrões foram concebidos tanto por empresas quanto pela comunidade, buscando resolver problemas comuns enfrentados em projetos do mundo real. Alguns desses modelos se destacaram tanto que se tornaram populares e hoje são amplamente utilizados em projetos ao redor do mundo.

MVC: Um túnel do tempo na história do Android

Retornando ao passado, quando “tudo era mato” em 2009, o desenvolvimento Android começava a ganhar força à medida que a plataforma crescia e se desenvolvia. Nesse período, um padrão de projeto destacava-se significativamente: o MVC. Você já deve ter ouvido falar dele, afinal, ele foi criado em meados dos anos 70. Esse padrão é empregado não apenas no Android, mas em muitas outras tecnologias também.

O MVC foi um padrão recomendado pela Google e pela comunidade na época para projetos no Android. Sua ideia era separar cada responsabilidade da aplicação em conceitos, visando proporcionar maior organização e escalabilidade ao projeto. Essa separação ocorre em três camadas:

Modelo MVC

Model: responsável pelo tratamento dos dados e das regras de negócio da aplicação, encarregando-se de armazenar e manipular os dados. É nesta camada que os dados do SQLite seriam acessados, assim como os provedores.

View: responsável pela parte visual da aplicação, onde a tela é manipulada, e são adicionados os componentes gráficos e seus comportamentos. Aqui são armazenados os arquivos XML dos layouts e os recursos.

Controller: responsável por facilitar a comunicação entre a View e a Model. Sempre que a View precisar manipular algo relacionado aos dados da Model, ela chamará a Controller para executar essa tarefa. A Controller também pode realizar validações e implementar algumas regras. Seguindo o modelo da época, é na Controller que o código Java seria desenvolvido.

Problemas do MVC:

  • Devido o XML ser estático, mudanças na interface acabam ocorrendo programaticamente na Activity ou na Fragment, que também controla chamadas de modelo, fazendo assim um papel de View e Controller.
  • Muitos componentes dependem de framework Android e por isso precisam ficar atrelados a Activity e Fragment fazendo que eles fiquem extensos e complexos.
  • Fazer testes unitários neste padrão era quase impossível devido ao alto acoplamento do código.

Na prática, era muito difícil dividir as responsabilidades neste modelo, a Controller se misturava com a View e com a Model, deixando o código extremamente confuso. O resultado nos projetos da época eram Activities gigantes que faziam de tudo.

Indo direto ao ponto:

O MVC foi muito útil na época em que os aplicativos eram bastante simples, contando com poucos recursos. No entanto, atualmente, as necessidades do mercado mudaram, e essa abordagem não consegue lidar com os desafios das aplicações modernas. Estudar o MVC é interessante para adquirir conhecimento e compará-lo com padrões mais modernos. No entanto, em projetos contemporâneos, não é uma opção viável devido a todas as suas desvantagens, que o tornam menos eficaz quando comparado a arquiteturas mais recentes.

MVP: As coisas começam a evoluir

Dado que o MVC estava uma bagunça para ser mantido nos projetos, a comunidade procurava uma solução para o problema, uma arquitetura mais consistente. Foi então que surgiu o MVP. Esse padrão teve origem em meados dos anos 90 e era inicialmente utilizado em linguagens como Java e C++. No entanto, ganhou destaque no Android por volta de 2010. Nesse modelo, observamos uma melhor divisão de responsabilidades, o que contribui para uma organização mais eficiente.

Modelo MVP

Vamos para a sopa de letrinhas:

View: responsável por apresentar informações de forma gráfica para o usuário e enviar eventos para o Presenter. Ela cuida de tudo relacionado à View e ao framework Android, é aqui onde armazenamos nossas Activities, Fragments e XMLs. A ideia do MVP é que a View não execute nenhum “if” ou qualquer outra condição, deixando esse trabalho totalmente para o Presenter. Aqui, observamos uma grande diferença em relação ao MVC, onde a Activity desempenhava diversas funções.

Presenter: contém a lógica de apresentação, realizando consultas ao modelo e formatando respostas para serem exibidas na View. Neste ponto, a Presenter é desacoplada da View, não tem conhecimento do que ocorre lá, ela apenas instrui o que deve ser feito. A Presenter segue o padrão POJO (Plain Old Java Object), sendo uma classe simples que não possui interdependências.

Model: onde a lógica de negócio é definida. É aqui que armazenamos os modelos de dados que serão posteriormente exibidos na tela.

A regra de ouro do MVP:

  • A View fala como algo deve ser apresentado.
  • O Presenter fala o que deve ser apresentado.

Aqui está o ponto crucial. Os testes unitários com o MVC eram quase impossíveis de serem realizados, enquanto no MVP, devido ao fato de a Presenter ser uma classe totalmente desacoplada e independente do framework Android, podemos facilmente testar nossas lógicas de apresentação. Em outras palavras, nossa View não deve conter nem mesmo um “if”; essa responsabilidade deve ser atribuída à Presenter.

Um pouco de código sempre é bom

Para exemplificar o funcionamento do MVP, vamos desenvolver uma aplicação simples, na qual nossa Activity solicita uma lista de usuários para a Presenter e os exibimos em uma RecyclerView.

Model (User.kt):

data class User(val id: Long, val name: String, val email: String)

Presenter (UserPresenter.kt):

interface UserContract {
 interface View {
 fun showUsers(users: List<User>)
    }

 interface Presenter {
 fun getUsers()
    }
}

class UserPresenter(private val view: UserContract.View) : UserContract.Presenter {
 override fun getUsers() {
 // Aqui você faria a lógica de negócios para obter a lista de usuários.
 // Neste exemplo, usaremos usuários fictícios.
 val users = listOf(
            User(1, "Josemaria Escrivá", "[email protected]"),
            User(2, "Francesco Forgione", "[email protected]"),
            User(3, "Carlos Acutis", "[email protected]")
        )

        view.showUsers(users)
    }
}

View (UserActivity.kt):

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.YourAppName.databinding.ActivityMainBinding

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.YourAppName.databinding.ActivityMainBinding

class UserActivity : AppCompatActivity(), UserContract.View {

    private lateinit var userPresenterUserPresenter
    private lateinit var bindingActivityMainBinding
    private lateinit var userAdapterUserAdapter

    override fun onCreate(savedInstanceStateBundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Inicializando a RecyclerView usando ViewBinding
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        userAdapter = UserAdapter()
        binding.recyclerView.adapter = userAdapter

        // Inicializando a Presenter
        userPresenter = UserPresenter(this)

        // Chamando explicitamente getUsers para carregar os dados
        userPresenter.getUsers()
    }

    override fun showUsers(usersList<User>) {
        // Atualizando o Adapter da RecyclerView com a nova lista de usuários
        userAdapter.submitList(users)
    }
}

No exemplo acima, podemos perceber que a View e o Presenter respeitam as regras impostas pela UserContract.kt, esta interface que vai ditar o que a View vai mostrar e o que a Presenter deve implementar.

Podemos ver também que a Presenter recebe uma instância deste contrato da View, é por essa instância que a Presenter vai pedir para a Activity fazer algo. Já a Activity (View) estende este contrato UserContract.View.kt, a ideia é que absolutamente tudo o que é relacionado a tela esteja dentro do contrato estabelecido.

Vantagens

  • Finalmente uma divisão de camadas coerente, onde cada uma tem uma responsabilidade única.
  • Você consegue testar 100% do seu código da lógica de apresentação com JUnit.
  • Baixo acoplamento, sendo possível utilizar a Presenter em outro lugares, também é possível modificar as informações que serão mostradas na View sem necessidade de modificar a Presenter.

Desvantagens

  • Para se comunicar com a VIew, tanto a View quanto a Presenter precisam ter uma instância um do outro, isso pode causar problemas de ciclo de vida e memory leaks.
  • Este problema pode ser contornado, mas é necessário muito código para garantir que estes problemas de ciclo de vidas não ocorram. Por isso o código acaba gerando mais complexidade ao projeto.

O gargalo do MVP: Os problemas de ciclo de vida

Conforme mencionado nas desvantagens em relação aos problemas do ciclo de vida, um exemplo muito comum que podemos citar é quando a Presenter tenta acessar a View, mas ela está destruída.

Tomando como base o código mostrado anteriormente, imagine a seguinte situação:

O usuário está com a tela aberta (activity), no momento em que a tela for construída, a activity deve carregar a lista de usuários, chamando o presenter.getUsers(). Em seguida, o Presenter vai chamar a Model para fazer a requisição na API. No entanto, neste intervalo de tempo, antes da chamada retornar, o usuário clica no botão voltar e o aplicativo quebra!

Ele quebra porque a Presenter, ao concluir o processo de “getUsers”, tentará chamar o método “showUsers” da View para exibir a lista de usuários. Entretanto, como a Activity foi destruída (sim, o botão voltar destrói a activity e a remove da pilha), a Presenter não a encontrará, resultando em uma exceção em Java. Como mencionado anteriormente, a Presenter recebe uma instância da View por meio do construtor e sempre espera que a Activity esteja ativa. Se ela não estiver, ocorrerá um crash.

Este problema pode ser contornado implementando tratamentos no controle das dependências da View e da Presenter. No entanto, isso pode ser bastante trabalhoso, gerando complexidade no projeto. Apesar disso, em minhas experiências profissionais, já vi muitos projetos que lidaram bem com esse problema, mantendo uma arquitetura saudável e flexível. O objetivo deste artigo não é aprofundar nestes detalhes, mas há bastante material disponível falando sobre esse assunto.

 

MVVM: uma nova abordagem

O MVVM foi criado em meados de 2005 por engenheiros da Microsoft e a arquitetura foi posteriormente difundida em diversas linguagens. No entanto, no Android, tornou-se mais conhecido por volta de 2015, quando a Google lançou a biblioteca de data binding durante o Google I/O de 2015. A arquitetura ganhou ainda mais popularidade após o lançamento dos Architecture Components no Google I/O de 2017, os quais introduziram no Android uma série de ferramentas que facilitam a implementação desse modelo arquitetural.

Modelo MVVM

Vamos para a sopa de letrinhas:

View: responsável por apresentar informações de forma gráfica para o usuário. Ele cuida de tudo que é relacionado a parte gráfica e ao framework Android, é aqui onde vamos armazenar nossas Activities, Fragments e XMLs. A ideia do MVVM é que a View cuide apenas das regras de tela, deixando o restante para a ViewModel e Model, aqui a View é bem semelhante com a View do MVP.

View Model: contém a lógica de apresentação, fazendo consultas ao modelo e formatando os dados. A ViewModel é totalmente desacoplada da View, ela não sabe de nada do que acontece lá. Um ponto fundamental, é que a ViewModel funciona de forma passiva, pois ela não guarda uma instância da View e aqui temos uma diferença grande para o MVP, pois a ViewModel apenas guarda os estados, não tendo uma relação direta com a View.

Model: Onde a lógica de negócio é definida. É aqui que armazenamos os modelos dos dados que vamos exibir posteriormente em tela. Neste ponto, nada de diferente dos outros modelos.

Diferenças em relação ao MVP

A principal distinção na abordagem do MVVM em comparação ao MVP é que:

  • No MVP, a Presenter recebe uma instância da View e, de certa forma, tem conhecimento das atividades que ocorrem nela.
  • No MVVM, a ViewModel não recebe instância alguma da View; ela apenas armazena dados, e a View se encarrega de entender o que está acontecendo.

Essa diferença resolve a principal desvantagem do MVP: os problemas de ciclo de vida. Como a ViewModel é completamente independente, ela não corre o risco de chamar uma implementação de uma activity que já foi destruída. Outros problemas e questões relacionadas ao ciclo de vida são tratados pelo próprio framework do Android quando utilizamos os Architecture Components.

Mas como a Viewmodel se comunica com a View?

Conforme mencionado anteriormente, a ViewModel funciona de forma passiva, ela apenas armazena os dados que podem ser usados na aplicação, a View por sua vez utiliza de alguns mecanismos para poder saber o que ocorre dentro da VM. Neste caso, a forma mais usada é com o architecture components, que nos fornece os famosos LiveDatas.

Os Live Datas armazenam os dados e a View observa suas mudanças para que possa mostrá-los em tela. A principal característica do LiveData é que ele garante que as atualizações de dados só sejam enviadas para os observadores (Activities e Fragments) quando esses componentes estão em um estado ativo. Isso ajuda a evitar vazamentos de memória e a garantir que as atualizações ocorram apenas quando são necessárias.

 

Como isso fica no código?

Usando o mesmo exemplo mostrado no MVP, vamos desenvolver uma aplicação simples, que irá carregar uma listagem de usuários num recyclerview no momento que a tela for aberta.

View Model (UserViewModel.kt):

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class UserViewModel : ViewModel() {

 private val _userData = MutableLiveData<List<User>>()

 // Expondo LiveData imutável para os observadores externos
 val userData: LiveData<List<User>>
 get() = _userData

 // Método para obter os dados (pode ser chamado para carregar os dados reais)
 fun getUsers() {
 // Aqui você faria a lógica de negócios para obter a lista de usuários.
 // Neste exemplo, usaremos dados fictícios.
 val users = listOf(
            User(1, "John Doe", "[email protected]"),
            User(2, "Jane Doe", "[email protected]"),
            User(3, "Bob Smith", "[email protected]")
        )

 // Atualizando o LiveData com os novos dados
        _userData.value = users
    }
}

View (UserActivity.kt):

class UserActivity : AppCompatActivity() {

 private lateinit var userViewModel: UserViewModel
 private lateinit var binding: ActivityMainBinding
 private lateinit var userAdapter: UserAdapter

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

 // Inicializando a RecyclerView usando ViewBinding
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        userAdapter = UserAdapter() // Substitua UserAdapter pelo nome do seu Adapter
        binding.recyclerView.adapter = userAdapter

 // Inicializando o ViewModel
        userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)

 // Observando as mudanças nos dados do ViewModel
        userViewModel.userData.observe(this, Observer { users ->
 // Atualizando a interface do usuário com a nova lista de usuários
            updateUI(users)
        })

 // Chamando explicitamente getUsers para carregar os dados
        userViewModel.getUsers()
    }

 private fun updateUI(users: List<User>) {
 // Atualizando o Adapter da RecyclerView com a nova lista de usuários
        userAdapter.submitList(users)
    }
}

Entendendo por partes

No momento que nossa UserActivity é carregada, criamos a instância da UserViewModel e em seguida definimos um observer para o userData da ViewModel. Este observer irá ser avisado toda vez que o userData for alterado. Após isso chamamos o método que deve carregar os usuário (getUsers). Veja em que momento algum, passamos alguma referência da View para a ViewModel.

Falando da UserViewModel, ela é a responsável por gerenciar os dados que serão apresentados na tela e fazer os tratamentos necessários. Nesta classe, criamos a instância do userData que será um MutableLiveData, devemos usá-lo para que a View possa observá-lo. Neste ponto, podemos notar que em momento algum a ViewModel se preocupa em como esses dados serão exibidos em tela, ela apenas gerencia os dados e atualiza o LiveData. E aqui está o ponto CHAVE do MVVM, a ViewModel é passiva, ela não deve se preocupar com nada a respeito da camada de View. Inclusive é recomendado que nunca se use nomes de função que digam respeito a visualização e interação de componentes de tela, palavras como: “show”, “hide”, “setColor”, “click”, entre outras, devem ser evitadas, pois a ideia é que a ViewModel não saiba destes detalhes da apresentação da interface.

Vantagens

  • O MVVM atualmente é o padrão mais utilizado no Android, então é comum encontrar desenvolvedores com conhecimento se você quiser criar um projeto do zero.
  • A Google recomenda o padrão em suas documentações e oferece uma serie de ferramentas, como LivaData e o Databing.
  • Arquitetura consistente que resolve muitos problemas de ciclo de vida que você teria com outras abordagens.
  • Compatibilidade com Jetpack Compose.

Desvantagens

  • A curva de aprendizado do MVVM é um pouco maior do que as demais, devido a abordagem passiva com o uso de liveDatas e observers.

Conclusão

Após analisar cada arquitetura, é possível compreender um pouco sobre os problemas e soluções que cada abordagem trouxe para o Android. No entanto, não podemos ficar em cima do muro quanto a “qual é a melhor arquitetura?”. Atualmente, entre os desenvolvedores Android, é quase uma unanimidade que o MVVM resolve boa parte dos problemas dos projetos atuais, razão pela qual a própria Google recomenda o uso desse padrão nas aplicações. O MVVM destaca-se por ser uma arquitetura consistente que se adapta bem a projetos de todos os portes, além de permitir a escalabilidade da aplicação de forma sólida, separando claramente as responsabilidades de cada camada.

Outro ponto importante é que existem muitos materiais na internet sobre o tema, e todas as novidades do Android oferecem suporte ao MVVM, muitas delas com recursos pensados especificamente para essa arquitetura, como o LiveData, lançado pela Google em 2017. Portanto, recomendo que, ao iniciar um projeto do zero, seja utilizado o MVVM.

No entanto, em projetos legados ou em projetos grandes e complexos que foram construídos com MVP, por exemplo, é interessante avaliar se vale a pena realizar uma migração de arquitetura, pois isso demandaria bastante esforço. Caso a aplicação já tenha solucionado os problemas de ciclo de vida do MVP (principal gargalo dessa arquitetura), não há motivos para refatoração.

Perceba que não existe uma bala de prata, mas sim soluções que se encaixam melhor em cada cenário. Espero que este artigo tenha adicionado novos conceitos à sua caixa de ferramentas! 😊

Referências

Guide to app architecture – Google

https://developer.android.com/topic/architecture

A história do MVC, MVP e MVVM para android | CI&T Tech Summit
https://www.youtube.com/watch?v=fG3Vr3RX2gg&amp;t=1500s&amp;ab_channel=CI%26T

Android MVVM — how to use MVVM in android example?

https://medium.com/@dheerubhadoria/android-mvvm-how-to-use-mvvm-in-android-example-7dec84a1fb73

Padrão de Arquitetura MVP no Android

https://medium.com/@alifyzfpires/padr%C3%A3o-de-arquitetura-mvp-no-android-23a6fa96a27b

MVP: Model-View-Presenter The Taligent Programming Model for C++ and Java

https://www.wildcrest.com/Potel/Portfolio/mvp.pdf

Outras publicações