• Marcio Granzotto Rodrigues
  • hace 4 años
  • Categorías:Opinión, Técnica
  • Etiquetas:Android, Arquitectura, desarrollo, iOS, kotiln, Móvil, VIPER

Comenzando como desarrollador de Android y más tarde trabajando también con iOS, tuve contacto con varias arquitecturas de proyectos diferentes, algunas buenas y otras malas.

Estaba felizmente usando la arquitectura MVP para Android hasta que conocí – y trabajé ocho meses con – la arquitectura VIPER en un proyecto de iOS. Cuando volví a Android, decidí adaptar e implementar VIPER en él, a pesar de que algunos otros desarrolladores sugerían que no tendría sentido utilizar una arquitectura de iOS en Android. Dada la diferencia fundamental entre los marcos de trabajo de Android y de iOS, tenía algunas preguntas sobre la utilidad de VIPER en Android. ¿Sería posible y valdría la pena el esfuerzo? Empecemos por lo básico.

¿Qué es VIPER?

VIPER es una arquitectura limpia utilizada principalmente en el desarrollo de aplicaciones para iOS. Ayuda a mantener el código limpio y organizado, evitando la situación de Massive-View-Controller.

VIPER significa View Interactor Presenter Entity Router, que son clases que tienen una responsabilidad bien definida, siguiendo el principio de responsabilidad única. Puedes leer más sobre ello en este excelente artículo.

Arquitecturas para Android

Ya hay algunas arquitecturas muy buenas para Android. Las más famosas son Model-View-ViewModel (MVVM) y Model-View-Presenter (MVP).

MVVM tiene mucho sentido si lo usas junto a data binding, y como no me gusta mucho la idea de data binding, siempre he usado MVP para los proyectos en los que he trabajado. Sin embargo, a medida que los proyectos crecen, el presentador puede convertirse en una clase enorme con un montón de métodos, lo que hace que sea difícil de mantener y entender. Esto sucede porque es responsable de muchas cosas: tiene que manejar los eventos de la interfaz de usuario, la lógica de la interfaz de usuario, la lógica de negocio, la red y las consultas a la base de datos. Eso viola el Principio de Responsabilidad Única, algo que VIPER puede arreglar.

¡Arreglémoslo!

Con estos problemas en mente, empecé un nuevo proyecto Android y decidí usar MVP + Interactor (o VIPE, si quieres). Eso me permitió trasladar parte de la responsabilidad del presentador al Interactor. Dejando al presentador con el manejo de los eventos de UI y la preparación de los datos que vienen del Interactor para ser mostrados en la Vista. Entonces, el Interactor sólo es responsable de la lógica de negocio y de obtener los datos de las bases de datos o de las APIs.

Además, empecé a utilizar interfaces para enlazar los módulos entre sí. De esta manera, no pueden acceder a otros métodos que no sean los declarados en la interfaz. Esto protege la estructura y ayuda a definir una responsabilidad clara para cada módulo, evitando errores del desarrollador como poner la lógica en el lugar equivocado. Así es como se ven las interfaces:

 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) } } 

Y aquí hay algo de código para ilustrar las clases que implementan esas interfaces (está en Kotlin, pero en Java debería ser igual).

 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!") }) } } 

El código completo está disponible en este Gist.

Puedes ver que los módulos son creados y enlazados juntos al inicio. Cuando se crea la Actividad, se inicializa el Presentador, pasándose a sí mismo como la Vista en el constructor. El Presentador entonces inicializa el Interactor pasándose a sí mismo como el InteractorOutput.

En un proyecto VIPER de iOS esto sería manejado por el Router, creando el UIViewController, u obteniéndolo de un Storyboard, y luego conectando todos los módulos juntos. Pero en Android no creamos las Activities nosotros mismos: tenemos que usar Intents, y no tenemos acceso a la Activity recién creada desde la anterior. Esto ayuda a prevenir las fugas de memoria, pero puede ser un dolor si sólo quieres pasar datos al nuevo módulo. Tampoco podemos poner el Presentador en los extras del Intent porque tendría que ser Parcelable o Serializable. Es simplemente imposible.

Por eso en este proyecto he omitido el Router. Pero, ¿es ese el caso ideal?

VIPE + Router

La implementación anterior de VIPE resolvió la mayoría de los problemas del MVP, dividiendo las responsabilidades del Presentador con el Interactor.

Sin embargo, la Vista no es tan pasiva como la Vista del VIPER de iOS. Tiene que manejar toda la responsabilidad regular de la Vista más el enrutamiento a otros módulos. Esto NO debería ser su responsabilidad y podemos hacerlo mejor. Entra en el Router.

Aquí están las diferencias entre «VIPE» y 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 disponible aquí.

Ahora trasladamos la lógica de enrutamiento de la vista al Router. Sólo necesita una instancia de la Activity para poder llamar al método startActivity. Todavía no está todo conectado como el VIPER de iOS, pero al menos respeta el Principio de Responsabilidad Única.

Conclusión

Habiendo desarrollado un proyecto con MVP + Interactor y ayudando a un compañero de trabajo a desarrollar un proyecto completo de VIPER Android, puedo decir con seguridad que la arquitectura sí funciona en Android y merece la pena. Las clases se hacen más pequeñas y más mantenibles. También guía el proceso de desarrollo, porque la arquitectura deja claro dónde debe escribirse el código.

Aquí en Cheesecake Labs estamos planeando utilizar VIPER en la mayoría de los nuevos proyectos, para poder tener una mejor mantenibilidad y un código más claro. Además, facilita el salto de un proyecto iOS a un proyecto Android y viceversa. Por supuesto, se trata de una adaptación en evolución, por lo que nada está grabado en piedra. Estaremos encantados de recibir comentarios al respecto.

Articles

Deja una respuesta

Tu dirección de correo electrónico no será publicada.