- Marcio Granzotto Rodrigues
- 4 anos atrás
- Categorias:Opinião, Técnico
- Tags:Android, Arquitetura, desenvolvimento, iOS, kotiln, Mobile, VIPER
Começando como desenvolvedor Android e mais tarde trabalhando com iOS também, tive contato com várias arquiteturas de projetos diferentes – alguns bons e outros ruins.
Estava feliz usando a arquitetura MVP para Android até que conheci – e trabalhei oito meses com – a arquitetura VIPER em um projeto iOS. Quando voltei ao Android, decidi adaptar e implementar o VIPER nele, apesar de alguns outros dispositivos sugerirem que não faria sentido usar uma arquitetura iOS no Android. Dada a diferença fundamental entre os frameworks do Android e do iOS, eu tinha algumas perguntas sobre a utilidade do VIPER para o Android. Seria viável e valeria a pena o esforço? Vamos começar com o básico.
O que é VIPER?
VIPER é uma arquitetura limpa, usada principalmente no desenvolvimento de aplicativos iOS. Ela ajuda a manter o código limpo e organizado, evitando a situação de Massive-View-Controller.
VIPER significa View Interactor Presenter Entity Router, que são classes que têm uma responsabilidade bem definida, seguindo o Princípio de Responsabilidade Única. Você pode ler mais sobre isso neste excelente artigo.
Android architectures
Já existem algumas arquiteturas muito boas para Android. A mais famosa sendo Model-View-ViewModel (MVVM) e Model-View-Presenter (MVP).
MVVM faz muito sentido se você usá-lo ao lado de data binding, e como eu não gosto muito da idéia de data binding, eu sempre usei MVP para os projetos em que trabalhei. Entretanto, à medida que os projetos crescem, o apresentador pode se tornar uma classe enorme com muitos métodos, tornando difícil a manutenção e a compreensão. Isso acontece porque é responsável por um monte de coisas: tem que lidar com eventos de IU, lógica de IU, lógica de negócios, networking e consultas a bancos de dados. Isso viola o Princípio da Responsabilidade Única, algo que VIPER pode consertar.
Vamos consertar isso!
Com esses problemas em mente, eu comecei um novo projeto Android e decidi usar MVP + Interactor (ou VIPE, se você preferir). Isso me permitiu mover alguma responsabilidade do apresentador para o Interactor. Deixando o apresentador com o tratamento de eventos UI e preparação dos dados que vêm do Interactor para serem exibidos no View. Então, o Interactor é responsável apenas pela lógica de negócio e buscar dados de DBs ou APIs.
Ainda, eu comecei a usar interfaces para ligar os módulos entre si. Dessa forma, eles não podem acessar outros métodos além dos declarados na interface. Isso protege a estrutura e ajuda a definir uma clara responsabilidade para cada módulo, evitando erros do desenvolvedor como colocar a lógica no lugar errado. Aqui está como as interfaces se parecem:
class LoginContracts { interface View { fun goToHomeScreen(user: User) fun showError(message: String) } interface Presenter { fun onDestroy() fun onLoginButtonPressed(username: String, password: String) } interface Interactor { fun login(username: String, password: String) } interface InteractorOutput { fun onLoginSuccess(user: User) fun onLoginError(message: String) } }
E aqui está algum código para ilustrar as classes que implementam essas interfaces (está em Kotlin, mas Java deve ser o mesmo).
class LoginActivity: BaseActivity, LoginContracts.View { var presenter: LoginContracts.Presenter? = LoginPresenter(this) override fun onCreate() { //... loginButton.setOnClickListener { onLoginButtonClicked() } } override fun onDestroy() { presenter?.onDestroy() presenter = null super.onDestroy() } private fun onLoginButtonClicked() { presenter?.onLoginButtonClicked(usernameEditText.text, passwordEditText.text) } fun goToHomeScreen(user: User) { val intent = Intent(view, HomeActivity::class.java) intent.putExtra(Constants.IntentExtras.USER, user) startActivity(intent) } fun showError(message: String) { //shows the error on a dialog } } class LoginPresenter(var view: LoginContracts.View?): LoginContracts.Presenter, LoginContracts.InteractorOutput { var interactor: LoginContracts.Interactor? = LoginInteractor(this) fun onDestroy() { view = null interactor = null } fun onLoginButtonPressed(username: String, password: String) { interactor?.login(username, password) } fun onLoginSuccess(user: User) { view?.goToNextScreen(user) } fun onLoginError(message: String) { view?.showError(message) } } class LoginInteractor(var output: LoginContracts.InteractorOutput?): LoginContracts.Interactor { fun login(username: String, password: String) { LoginApiManager.login(username, password) ?.subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe({ //does something with the user, like saving it or the token output?.onLoginSuccess(it) }, { output?.onLoginError(it.message ?: "Error!") }) } }
O código completo está disponível neste Gist.
Você pode ver que os módulos são criados e ligados entre si na inicialização. Quando a Atividade é criada, ela inicializa o Apresentador, passando a si mesma como a Vista no construtor. O Apresentador então inicializa o Interactor passando a si mesmo como o InteractorOutput
.
Em um projeto iOS VIPER isto seria tratado pelo Roteador, criando o UIViewController
, ou obtendo-o de um Storyboard, e então ligando todos os módulos juntos. Mas no Android não criamos as atividades nós mesmos: temos que usar Intents, e não temos acesso à atividade recém-criada a partir da anterior. Isto ajuda a prevenir fugas de memória, mas pode ser uma chatice se apenas quiseres passar dados para o novo módulo. Também não podemos colocar o Apresentador nos extras da Intent porque teria de ser Parcelable
ou Serializable
. Não é possível fazer.
É por isso que neste projeto eu omiti o Roteador. Mas é esse o caso ideal?
VIPE + Router
A implementação acima do VIPE resolveu a maioria dos problemas do MVP, dividindo as responsabilidades do Apresentador com o Interactor.
No entanto, a Vista não é tão passiva como a Vista do iOS VIPER. Ele tem que lidar com toda a responsabilidade regular da View mais o encaminhamento para outros módulos. Isto NÃO deve ser sua responsabilidade e nós podemos fazer melhor. Entre no Roteador.
Aqui estão as diferenças entre “VIPE” e VIPER:
class LoginContracts { interface View { fun showError(message: String) //fun goToHomeScreen(user: User) //This is no longer a part of the View's responsibilities } interface Router { fun goToHomeScreen(user: User) // Now the router handles it } } class LoginPresenter(var view: LoginContracts.View?): LoginContracts.Presenter, LoginContracts.InteractorOutput { //now the presenter has a instance of the Router and passes the Activity to it on the constructor var router: LoginContracts.Router? = LoginRouter(view as? Activity) //... fun onLoginSuccess(user: User) { router?.goToNextScreen(user) } //... } class LoginRouter(var activity: Activity?): LoginContracts.Router { fun goToHomeScreen(user: User) { val intent = Intent(view, HomeActivity::class.java) intent.putExtra(Constants.IntentExtras.USER, user) activity?.startActivity(intent) } }
Código completo disponível aqui.
Agora nós movemos a lógica de roteamento da View para o Roteador. Ele só precisa de uma instância da Atividade para que ele possa chamar o método startActivity
. Ele ainda não liga tudo junto como o iOS VIPER, mas pelo menos respeita o Princípio de Responsabilidade Única.
Conclusion
Having desenvolveu um projeto com MVP + Interactor e ao ajudar um colega de trabalho a desenvolver um projeto VIPER Android completo, posso dizer com segurança que a arquitetura funciona no Android e vale a pena. As classes se tornam menores e mais fáceis de manter. Ele também orienta o processo de desenvolvimento, pois a arquitetura deixa claro onde o código deve ser escrito.
Here on Cheesecake Labs estamos planejando usar o VIPER na maioria dos novos projetos, para que possamos ter melhor manutenção e código mais claro. Além disso, facilita saltar de um projeto iOS para um projeto Android e vice-versa. Claro que esta é uma adaptação evolutiva, por isso nada aqui é esculpido em pedra. Agradecemos de bom grado algum feedback sobre isso!