MVVM con Architecture Components: Una guía paso a paso para los amantes de MVP
Antonio Leiva

Bien, ahora que MVVM es el estándar para implementar aplicaciones de Android desde que Google lanzó su Guía de arquitectura de aplicaciones, creo que es el momento de proporcionar información fácil para comprender el patrón de MVVM desde la perspectiva de un usuario de MVP.

Si has llegado aquí por casualidad, pero no sabes lo que es MVP o cómo usarlo en Android, te recomiendo que primero eches un vistazo a este artículo sobre el tema que escribí hace algún tiempo.

También tienes la opción de ver este contenido en YouTube:

MVVM vs MVP – ¿Necesito refactorizar todas mis app ahora?

Durante mucho tiempo, MVP parece ha sido el patrón de presentación más utilizado para aislar a la interfaz de usuario de la lógica de negocio, pero ahora hay un nuevo sheriff en la ciudad.

Muchas personas me preguntaron si deberían huir de MVP o qué hacer cuando inician una nueva aplicación. Estos son algunas ideas al azar al respecto:

  • MVP no esta muerto. Sigue siendo un patrón perfectamente válido que puede seguir usando como antes.
  • MVVM como patrón, no es necesariamente mejor. La implementación específica que hizo Google tiene sentido, pero hay una razón por la cual MVP se usaba antes: simplemente encaja muy bien con el framework de Android y con poca complejidad.
  • Usar MVP no significa que no se puedan usar el resto de los componentes de la arquitectura. Probablemente el ViewModel no tiene mucho sentido (ya que es el sustituto natural del presentador), pero el resto de los componentes se pueden usar de una forma u otra.
  • No necesitas refactorizar tu aplicación de inmediato. Si estás contento con MVP, continúa con ello. En general, es mejor mantener una arquitectura sólida en lugar de tener todas las nuevas tendencias implementadas en diferentes pantallas de la aplicación. Refactorizaciones como esta no vienen sin coste añadido.

Diferencias entre MVVM y MVP

Afortunadamente, si ya conocse MVP ¡Aprender MVVM es extremadamente fácil! Solo hay una pequeña diferencia, al menos en la forma en que generalmente se aplica a Android:

En MVP, el presentador se comunica con la vista a través de una interfaz. En MVVM, el ViewModel se comunica con la vista usando el patrón Observer.

Sé que, si lees la definición original del patrón MVVM, no coincidirá exactamente con lo que dije antes. Pero en el caso particular de Android, y si dejamos databiding a un lado, en mi opinión esta es la mejor manera de entender cómo funciona.

Migrando de MVP a MVVM sin Arch Components

Lo que estoy haciendo aquí es adaptar el ejemplo que hice para MVP (puedes echar un vistazo al repositorio aquí) para usar MVVM. El nuevo repositorio está aquí.

Estoy excluyendo los Architecture Components por ahora, para que absorbamos la idea primero. Luego podemos ver cómo funciona el nuevo “framework” que Google creó, los puntos en los que facilita las cosas.

Creando una clase Observable

Como estamos usando el patrón Observer, necesitamos una clase que se pueda observar. Esta clase contendrá los observadores y un tipo genérico para el valor que se enviará a estos observadores. Cuando el valor cambia, los observadores son notificados:

class Observable<T> {

    private var observers = emptyList<(T) -> Unit>()

    fun addObserver(observer: (T) -> Unit) {
        observers += observer
    }

    fun clearObservers() {
        observers = emptyList()
    }

    fun callObservers(newValue: T) {
        observers.forEach { it(newValue) }
    }

}

Usando estados que representan las UI

Como ahora no tenemos una manera de comunicarnos directamente con la vista, no podemos decirle qué hacer. La forma en que encontré más flexible es tener modelos que representen el estado de la interfaz de usuario.

Por ejemplo, si queremos que muestre su progreso, enviaremos el estado Loading. La forma de consumir ese estado depende totalmente de la vista.

Para este caso en particular, creé una clase llamada ScreenState, el cual acepta un tipo genérico que representará el estado específico que necesita la view.

Puede haber algunos estados genéricos que se apliquen a todas las pantallas, como Loading (también podría pensar acerca de un estado de Error) y luego uno específico por pantalla.

El ScreenState general se puede modelar usando una sealed class como esta:

sealed class ScreenState<out T> {
    object Loading : ScreenState<Nothing>()
    class Render<T>(val renderState: T) : ScreenState<T>()
}

Entonces los estados específicos pueden tener cualquier estructura que necesitemos. Para el estado de inicio de sesión, un enumerado es suficiente:

enum class LoginState {
    Success, WrongUserName, WrongPassword
}

Pero para MainState, ya que estamos mostrando una lista de elementos y un mensaje, el enum no nos daría suficiente flexibilidad. Así que la sealed class es de nuevo extremadamente útil (veremos más adelante por qué):

sealed class MainState {
    class ShowItems(val items: List<String>) : MainState()
    class ShowMessage(val message: String) : MainState()
}

Convirtiendo los presentadores a ViewModels

Lo primero que ya no necesitamos es la interfaz View. Puedes deshacerte de ella ( y del argumento View) , porque usaremos un observable en su lugar.

Para definirlo:

val stateObservable = Observable<ScreenState<LoginState>>()

Luego, cuando queremos mostrar un progreso que indica que se está ejecutando un proceso, simplemente llamamos a los observadores con el estado Loading.

fun validateCredentials(username: String, password: String) {
    stateObservable.callObservers(ScreenState.Loading)
    loginInteractor.login(username, password, this)
}

Cuando el inicio de sesión finaliza, le pedimos que muestre el éxito:

override fun onSuccess() {
    stateObservable.callObservers(ScreenState.Render(LoginState.Success))
}

Realmente este estado anterior se puede modelar de muchas maneras diferentes. Si queremos ser más explícitos, podríamos decir que puede navegar a la pantalla principal con un LoginState.NavigateToMain o similar.

Pero como esto depende de muchos factores dependiendo de la estructura de la aplicación, lo dejaré así.

Luego, en el onDestroy del ViewModel, eliminamos a los observadores para no leakearlos.

fun onDestroy() {
    stateObservable.clearObservers()
}

Usando el ViewModel de la Actividad

La Actividad ahora no puede actuar como la View de ViewModel, de modo que ahí es donde el patrón de observador se pone en acción.

Primero, crea una propiedad que contenga el ViewModel:

private val viewModel = LoginViewModel(LoginInteractor())

Luego, en onCreate, puedes comenzar a observar el estado. Cuando el estado se actualice, llamará al método updateUI:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    viewModel.stateObservable.addObserver(::updateUI)
}

Aquí, gracias a las sealed classes y las enums, al usar la expresión when, todo se vuelve bastante fácil. Estoy procesando el estado en dos pasos: primero los estados generales y luego el LoginState particular.

El primer when mostrará un progreso con el estado de Loading, y llamará a otra función si tiene que representar el estado específico:

private fun updateUI(screenState: ScreenState<LoginState>) {
    when (screenState) {
        ScreenState.Loading -> progress.visibility = View.VISIBLE
        is ScreenState.Render -> processLoginState(screenState.renderState)
    }
}

Este segundo oculta el progreso (en caso de que estuviera visible), navega a la siguiente actividad si el inicio de sesión fue exitoso o muestra un error según el tipo de error:

private fun processLoginState(renderState: LoginState) {
    progress.visibility = View.GONE
    when (renderState) {
        LoginState.Success -&gt; startActivity(Intent(this, MainActivity::class.java))
        LoginState.WrongUserName -&gt; username.error = getString(R.string.username_error)
        LoginState.WrongPassword -&gt; password.error = getString(R.string.password_error)
    }
}

Cuando se hace clic en el botón, llamamos a ViewModel para que pueda hacer su trabajo:

private fun onLoginClicked() {
    viewModel.onLoginClicked(username.text.toString(), password.text.toString())
}

Y luego, en onDestroy(), llamamos a destruir el ViewModel (para que pueda limpiar los observadores):

override fun onDestroy() {
    viewModel.onDestroy()
    super.onDestroy()
}

Cambiando el código para usar los  Architecture Components

Por ahora, hemos tomado una solución a medida como un paso intermedio a MVVM, para que pueda ver las diferencias fácilmente. Hasta ahora, no hay muchos beneficios en comparación con MVP.

Pero sí que hay algunos. El más importante es te puedes olvidar de si la actividad se destruye o no, por lo que puedes desvincularte de su ciclo de vida y hacer tu trabajo en cualquier momento. Gracias a ViewModelLiveData, no necesitas preocuparte cuando la actividad se recrea o cuando se destruye.

Así es como funciona: mientras se recrea la actividad, ViewModel se mantiene vivo. Es justo cuando la actividad termina para siempre, cuando se llama al método onCleared() de ViewModel.

                   Tomado de  developers.android.com

Como LiveData también es consciente del ciclo de vida, sabe cuándo debe engancharse y desconectarse del propietario del ciclo de vida, por lo que no es necesario que lo hagas tú.

No quiero profundizar más en cómo funcionan los Architecture Components (se explica detalladamente en la guía para desarrolladores, pero puedo escribir otro artículo si crees que puede ser de ayuda), así que continuemos con la implementación.

Para usar los Architecture Components en nuestro proyecto, necesitamos agregar esta dependencia:

implementation "android.arch.lifecycle:extensions:1.1.1"

Hay varias bibliotecas para los architecture components y diferentes formas de incluirlos (dependiendo de si usas AndroidX o no, por ejemplo). Así que si tienes necesidades especiales, echa un vistazo aquí.

Architecture Components ViewModel

Para cambiar a ViewModel, solo necesitas extender la clase de la librería:

class LoginViewModel(private val loginInteractor: LoginInteractor) : ViewModel()

Elimine onDestroy(), ya no hace falta. Podemos mover su código a onCleared(). Así que de esa manera no necesitamos observar en onCreatey dejar de observar en onDestroy, sino que lo hacemos justo cuando se está borrando el ViewModel.

override fun onCleared() {
    stateObservable.clearObservers()
    super.onCleared()
}

Ahora, volviendo a la actividad, crea una property para el  ViewModel. Es necesario que se inicie lateinit porque se asignará en onCreate:

private lateinit var viewModel : LoginViewModel

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    viewModel = ViewModelProviders.of(this)[LoginViewModel::class.java]
}

Ese es el caso ideal, cuando el ViewModel no recibe argumentos. Pero si queremos que el ViewModel reciba argumentos a través del constructor, debes declarar una Factory. Este es el camino (un poco complicado, lo sé … ):

class LoginViewModelFactory(private val loginInteractor: LoginInteractor) :
    ViewModelProvider.NewInstanceFactory() {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return LoginViewModel(loginInteractor) as T
    }
}

Y para recuperar el ViewModel, el código cambia un poco:

ViewModelProviders.of(
    this,
    LoginViewModelFactory(LoginInteractor())
)[LoginViewModel::class.java]

Reemplazar Observable por LiveData

El LiveData puede sustituir nuestra clase Observable sin problema. Una cosa a tener en cuenta es que LiveData es inmutable por defecto (no puedes cambiar su valor).

Esto es genial, porque queremos que sea público para que los observadores puedan suscribirse, pero no queremos que otras partes del código cambien el valor.

Pero, por otro lado, los datos deben ser mutables, ¿para qué lo observaríamos si no va a cambiar? Para conseguir esto, el truco es usar el equivalente a un campo privado más un getter público con un tipo de retorno más restrictivo. En el caso de Kotlin, sería una propiedad privada más una pública:

private val _loginState: MutableLiveData<ScreenState<LoginState>> = MutableLiveData()

val loginState: LiveData<ScreenState<LoginState>>
    get() = _loginState

Y ya no necesitamos onCleared(), ya que LiveData también está suscrito al ciclo de vida. Sabrá el momento adecuado para dejar de ser observado.

Para observarlo, la forma más limpia es la siguiente:

viewModel.loginState.observe(::getLifecycle, ::updateUI)

Echa un vistazo a mi artículo sobre referencias de funciones si encuentras esta línea confusa.

La definición de updateUI requiere un ScreenState como argumento, para que se ajuste al valor de retorno de LiveData  y puedo usarlo como referencia de función:

private fun updateUI(screenState: ScreenState<LoginState>?) {
    ...
}

El MainViewModel tampoco requiere onResume(). En su lugar, podemos anular el getter de la property y ejecutar la solicitud la primera vez que se observe el LiveData:

private lateinit var _mainState: MutableLiveData<ScreenState<MainState>>

val mainState: LiveData<ScreenState<MainState>>
    get() {
        if (!::_mainState.isInitialized) {
            _mainState = MutableLiveData()
            _mainState.value = ScreenState.Loading
            findItemsInteractor.findItems(::onItemsLoaded)
        }
        return _mainState
    }

Y el código para esta actividad es muy similar al otro:

viewModel.mainState.observe(::getLifecycle, ::updateUI)

Para terminar

El código anterior parece  un poco más complicado, pero eso se debe principalmente al uso de un nuevo framework y hasta que haces a su uso.

Es verdad que hay nuevo boilerplate, como el de ViewModelFactory, la búsqueda de ViewModel, o las dos properties necesarias para evitar la edición de LiveData desde fuera. Simplifiqué algunas de ellas en este artículo usando algunas funciones de Kotlin y pueden ayudarte a sentirte más cómodo con el código. Pero no quería usarlos aquí por simplicidad.

Como mencioné al principio, si decides usar MVP o MVVM es totalmente tu decisión. No creo que haya una necesidad de migrar si tu arquitectura está ya resueta utilizando MVP, pero es interesante tener una idea de cómo funciona MVVM porque lo necesitarás tarde o temprano.

Creo que todavía estamos en un punto en el que estamos tratando de descubrir las mejores formas de usar MVVM con Architecture Components en nuestras aplicaciones Android y estoy seguro de que mi solución no es perfecta. Así que hazme saber todo lo que te preocupa o con lo que no estás de acuerdo, y estaré encantado de actualizar el artículo con tus comentarios.

Recuerda que tienes acceso al código completo en mi Github (las estrellas siempre se agradecen 😁)

Quizá también te interese…

Cómo simular una base de datos reactiva en Room con Fakes

Cómo simular una base de datos reactiva en Room con Fakes

En el desarrollo de aplicaciones móviles es muy común utilizar bases de datos para almacenar y gestionar la información que se utiliza en la aplicación. En el caso de Android, una de las opciones más populares es Room, una librería de persistencia de datos que...

Descargar una página web en Android con OkHttp

Descargar una página web en Android con OkHttp

En este tutorial vamos a aprender cómo descargar una página web en Android utilizando la librería OkHttp y la librería activity-ktx para facilitar el manejo de los ciclos de vida de nuestra aplicación. Configuración de la App Para empezar, necesitamos incluir las...

Usar Ktor Client para hacer peticiones HTTP en Android

Usar Ktor Client para hacer peticiones HTTP en Android

Ktor es un framework de servidor y cliente de Kotlin diseñado para crear aplicaciones web y móviles de forma rápida y fácil. En este artículo, veremos cómo usar Ktor client en una aplicación Android para hacer peticiones a una API. Configurar las dependencias de Ktor...

2 Comentarios

  1. Sergio Carillo

    Buenas Antonio me parece muy interesante el articulo, ¿Podría conseguir el código de prueba en formato JAVA?

    Un Saludo.

    Responder

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de DevExpert, S.L.U., con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *