Pourquoi vous devriez jeter MVP hors de vos projets

Bonjour à tous! Aujourd'hui, je voudrais parler de l'architecture des applications Android.
En fait, je n'aime pas vraiment les rapports et les articles sur ce sujet, mais récemment j'en suis venu à la réalisation avec laquelle je voudrais partager.


Quand j'ai commencé à me familiariser avec les architectures, mes yeux sont tombés sur MVP. J'ai aimé la simplicité et la disponibilité d'une énorme quantité de matériel de formation.
Mais au fil du temps, j'ai commencé à remarquer que quelque chose n'allait pas. Il y avait un sentiment qu'il est possible de mieux.


Presque toutes les implémentations que j'ai vues ressemblaient à ceci: nous avons une classe abstraite dont nous héritons tous nos présentateurs.


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

Nous créons également une interface d'affichage pour chaque écran, avec laquelle le présentateur travaillera


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

Examinons les inconvénients de cette approche:


  1. Vous devez créer une interface d'affichage pour chaque écran. Sur les grands projets, nous aurons beaucoup de code et de fichiers supplémentaires qui rendent la navigation dans les packages difficile.
  2. Le présentateur est difficile à réutiliser, car il est lié à la vue et il peut avoir des méthodes spécifiques.
  3. Une condition spécifique manque. Imaginez que nous faisons une demande au réseau, et en ce moment notre activité se meurt et une nouvelle est en train de se créer. Les données sont arrivées lorsque View n'est pas encore lié à Presenter. Cela soulève la question de savoir comment afficher ces données lorsque la vue est liée à Presenter? Réponse: uniquement des béquilles. Moxy, par exemple, a un ViewState qui stocke la liste ViewCommand. Cette solution fonctionne, mais il me semble que faire glisser le code pour enregistrer l'état View est superflu (le multidex est beaucoup plus proche que vous ne le pensez. De plus, l'assembly commencera à traiter les annotations, ce qui le rendra plus long. Oui, vous direz que nous avons maintenant kapt incrémental, mais certaines conditions sont nécessaires à son fonctionnement). De plus, ViewCommand ne sont ni parcellables ni sérialisables, ce qui signifie que nous ne pouvons pas les enregistrer en cas de décès du processus. Il est important d'avoir un état persistant pour ne rien perdre. De plus, l'absence d'un certain état ne permet pas de le modifier de manière centrale, ce qui peut entraîner des bogues difficiles à reproduire.

Voyons si ces problèmes sont résolus dans d'autres architectures.


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

Passons en revue les points mentionnés ci-dessus:


  1. Dans MVVM, VIew n'a plus d'interface, car il s'abonne simplement aux champs observables dans le ViewModel.
  2. ViewModel est plus facile à réutiliser car il ne connaît rien à View. (découle du premier paragraphe)
  3. Dans MVVM, le problème d'état est résolu, mais pas complètement. Dans cet exemple, nous avons une propriété dans le ViewModel, d'où View prend les données. Lorsque nous faisons une demande au réseau, les données seront enregistrées dans la propriété et View recevra des données valides lors de l'inscription (et vous n'avez même pas besoin de danser avec un tambourin). Nous pouvons également rendre le bien persistant, ce qui permettra de les conserver en cas de mort du procédé.

MVI


Définissez les actions, les effets secondaires et l'état


 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 ) 

Vient ensuite le réducteur


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

et EffectHandler pour gérer les effets secondaires


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

Qu'avons-nous:


  1. Dans MVI, nous n'avons pas non plus besoin de créer un tas de contrats pour View. Il vous suffit de définir la fonction de rendu (état).
  2. Réutiliser, malheureusement, n'est pas si simple, car nous avons State, qui peut être assez spécifique.
  3. Dans MVI, nous avons un certain état que nous pouvons changer de manière centrale via la fonction de réduction. Grâce à cela, nous pouvons suivre les changements d'état. Par exemple, écrivez toutes les modifications dans le journal. Ensuite, nous pouvons lire le dernier état si l'application plante. De plus, l'état peut être persistant, ce qui vous permet de gérer la mort du processus.

Résumé


MVVM résout le problème de la mort du processus. Mais, malheureusement, l'état ici est encore incertain et ne peut pas changer de façon centrale. C'est, bien sûr, un inconvénient, mais la situation est encore nettement meilleure que dans MVP. MVI résout le problème d'état, mais l'approche elle-même peut être un peu compliquée. De plus, il y a un problème avec l'interface utilisateur, car la boîte à outils d'interface utilisateur actuelle dans Android est mauvaise. Dans MVVM, nous mettons à jour l'interface utilisateur par morceaux, et dans MVI, nous nous efforçons de la mettre à jour dans son ensemble. Par conséquent, pour une interface utilisateur impérative, MVVM se comportera mieux. Si vous souhaitez utiliser MVI, je vous conseille de vous familiariser avec la théorie des DOM et bibliothèques virtuels / incrémentiels pour android: litho, enclume, jetpack compose (il faut attendre). Ou vous pouvez prendre des diffs avec vos mains.


Sur la base de toutes les données ci-dessus, je vous conseille de choisir entre MVVM et MVI lors de la conception d'une application. Vous obtenez donc une approche plus moderne et plus pratique (en particulier dans les réalités d'Android).


Bibliothèques pouvant aider à mettre en œuvre ces approches:
MVVM - https://github.com/Miha-x64/Lychee
MVI - https://github.com/egroden/mvico , https://github.com/badoo/MVICore , https://github.com/arkivanov/MVIDroid


Merci à tous pour votre attention!

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


All Articles