¿Por qué deberías echar MVP de tus proyectos?

Hola a todos! Hoy me gustaría hablar sobre la arquitectura de las aplicaciones de Android.
De hecho, realmente no me gustan los informes y artículos sobre este tema, pero recientemente he llegado a la conclusión con la que me gustaría compartir.


Cuando comencé a familiarizarme con las arquitecturas, mis ojos se posaron en MVP. Me gustó la simplicidad y la disponibilidad de una gran cantidad de materiales de capacitación.
Pero con el tiempo, comencé a notar que algo andaba mal. Hubo la sensación de que es posible mejor.


Casi todas las implementaciones que vi se veían así: tenemos una clase abstracta de la que heredamos todos nuestros presentadores.


class MoviePresenter(private val repository: Repository) : BasePresenter<MovieView>() { fun loadMovies() { coroutineScope.launch { when (val result = repository.loadMovies()) { is Either.Left -> view?.showError() is Either.Right -> view?.showMovies(result.value) } } } } 

También hacemos una interfaz de visualización para cada pantalla, con la cual trabajará el presentador


 interface MovieView : MvpView { fun showMovies(movies: List<Movie>) fun showError() } 

Veamos las desventajas de este enfoque:


  1. Tienes que crear una interfaz de Vista para cada pantalla. En proyectos grandes, tendremos muchos códigos y archivos adicionales que dificultarán la navegación de paquetes.
  2. Presentador es difícil de reutilizar, ya que está vinculado a la Vista, y puede tener métodos específicos.
  3. Falta una condición específica. Imagine que estamos haciendo una solicitud a la red, y en este momento nuestra actividad está muriendo y se está creando una nueva. Los datos llegaron cuando View aún no está vinculado a Presenter. Esto plantea la pregunta de cómo mostrar estos datos cuando la Vista está vinculada a Presentador. Respuesta: solo muletas. Moxy, por ejemplo, tiene un ViewState que almacena la lista ViewCommand. Esta solución funciona, pero me parece que arrastrar el código para guardar el estado Ver es superfluo (multidex está mucho más cerca de lo que piensas. Además, el ensamblaje comenzará a procesar anotaciones, lo que lo hará más largo. Sí, dirás que ahora tenemos kapt incremental, pero ciertas condiciones son necesarias para su funcionamiento). Además, ViewCommand no es Parcelable o Serializable, lo que significa que no podemos guardarlos en caso de muerte del proceso. Es importante tener un estado persistente para no perder nada. Además, la ausencia de un cierto estado no permite que se cambie centralmente, y esto puede conducir a errores difíciles de reproducir.

Veamos si estos problemas se resuelven en otras arquitecturas.


MVVM


 class MovieViewModel(private val repository: Repository) { val moviesObservable: ObservableProperty<List<Movie>> = MutableObservableProperty() val errorObservable: ObservableProperty<Throwable> = MutableObservableProperty() fun loadMovies() { coroutineScope.launch { when (val result = repository.loadMovies()) { is Either.Left -> errorObservable.value = result.value is Either.Right -> moviesObservable.value = result.value } } } } 

Veamos los puntos mencionados anteriormente:


  1. En MVVM, VIew ya no tiene una interfaz, ya que simplemente se suscribe a campos observables en ViewModel.
  2. ViewModel es más fácil de reutilizar porque no sabe nada sobre View. (se desprende del primer párrafo)
  3. En MVVM, el problema de estado está resuelto, pero no completamente. En este ejemplo, tenemos una propiedad en ViewModel, de donde View toma los datos. Cuando realizamos una solicitud a la red, los datos se almacenarán en la propiedad y View recibirá datos válidos al suscribirse (y ni siquiera tiene que bailar con una pandereta). También podemos hacer que la propiedad sea persistente, lo que permitirá que se conserven en caso de que falle el proceso.

MVI


Definir acciones, efectos secundarios y estado


 sealed class Action { class LoadAction(val page: Int) : Action() class ShowResult(val result: List<Movie>) : Action() class ShowError(val error: Throwable) : Action() } sealed class SideEffect { class LoadMovies(val page: Int) : SideEffect() } data class State( val loading: Boolean = false, val data: List<Movie>? = null, val error: Throwable? = null ) 

Luego viene Reductor


 val reducer = { state: State, action: Action -> when (action) { is Action.LoadAction -> state.copy(loading = true, data = null, error = null) to setOf( SideEffect.LoadMovies(action.page) ) is Action.ShowResult -> state.copy( loading = false, data = action.result, error = null ) to emptySet() is Action.ShowError -> state.copy( loading = false, data = null, error = action.error ) to emptySet() } } 

y EffectHandler para manejar SideEffects


 class MovieEffectHandler(private val movieRepository: MovieRepository) : EffectHandler<SideEffect, Action> { override fun handle(sideEffect: SideEffect) = when (sideEffect) { is SideEffect.LoadMovies -> flow { when (val result = movieRepository.loadMovies(sideEffect.page)) { is Either.Left -> emit(Action.ShowError(result.value)) is Either.Right -> emit(Action.ShowResult(result.value)) } } } } 

Que tenemos


  1. En MVI, tampoco necesitamos crear un montón de contratos para View. Solo necesita definir la función render (Estado).
  2. Reutilizar esto, desafortunadamente, no es tan simple, ya que tenemos State, que puede ser bastante específico.
  3. En MVI, tenemos un cierto estado que podemos cambiar centralmente a través de la función de reducción. Gracias a esto, podemos rastrear los cambios de estado. Por ejemplo, escriba todos los cambios en el registro. Entonces podemos leer el último estado si la aplicación falla. Plus State puede ser persistente, lo que le permite manejar la muerte del proceso.

Resumen


MVVM resuelve el problema de la muerte del proceso. Pero, desafortunadamente, el estado aquí todavía es incierto y no puede cambiar centralmente. Esto, por supuesto, es un inconveniente, pero la situación aún mejoró claramente que en MVP. MVI resuelve el problema de estado, pero el enfoque en sí puede ser un poco complicado. Además, hay un problema con la interfaz de usuario, ya que el kit de herramientas de la interfaz de usuario actual en Android es malo. En MVVM, actualizamos la IU en partes, y en MVI nos esforzamos por actualizarla en su conjunto. Por lo tanto, para una interfaz de usuario imperativa, MVVM se comportará mejor. Si desea utilizar MVI, le aconsejo que se familiarice con la teoría del DOM virtual / incremental y las bibliotecas para Android: litho, yunque, jetpack compose (debe esperar). O puede tomar diferencias con sus manos.


Con base en todos los datos anteriores, le aconsejaría que elija entre MVVM y MVI al diseñar una aplicación. Entonces obtienes un enfoque más moderno y conveniente (especialmente en las realidades de Android).


Bibliotecas que pueden ayudar a implementar estos enfoques:
MVVM - https://github.com/Miha-x64/Lychee
MVI - https://github.com/egroden/mvico , https://github.com/badoo/MVICore , https://github.com/arkivanov/MVIDroid


¡Gracias a todos por su atención!

Source: https://habr.com/ru/post/467983/


All Articles