Why you should throw MVP out of your projects

Hello! Today I would like to talk about the architecture of Android applications.

In fact, I do not really like reports and articles on this topic, but recently I have come to the realization with which I would like to share.







When I first started acquaintance with architectures, my eyes fell on MVP. I liked the simplicity and the availability of a huge amount of training materials.

But over time, I began to notice that something was wrong. There was a feeling that it is possible better.







Almost all the implementations that I saw looked like this: we have an abstract class from which we inherit all our presenters.







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





We also make a view interface for each screen, with which presenter will work







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





Let's look at the disadvantages of this approach:







  1. You have to create a View interface for each screen. On large projects, we will have a lot of extra code and files that make package navigation difficult.
  2. Presenter is difficult to reuse, as it is tied to the View, and it can have specific methods.
  3. A specific condition is missing. Imagine that we are making a request to the network, and at this moment our activity is dying and a new one is being created. Data came when View is not yet tied to Presenter. This begs the question of how to show this data when the View is bound to Presenter? Answer: only crutches. Moxy, for example, has a ViewState that stores the ViewCommand list. This solution works, but it seems to me that dragging the code to save the View state is superfluous (multidex is much closer than you think. Plus, the assembly will start processing annotations, which will make it longer. Yes, you will say that we now have incremental kapt, but certain conditions are necessary for its operation). Plus ViewCommand are not Parcelable or Serializable, which means that we cannot save them in case of death of the process. It is important to have a persistent state so as not to lose anything. Also, the absence of a certain state does not allow it to be changed centrally, and this can lead to difficult to reproduce bugs.


Let's see if these problems are solved in other 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 } } } }
      
      





Let's go through the items noted above:







  1. In MVVM, VIew no longer has an interface, as it simply subscribes to observable fields in the ViewModel.
  2. ViewModel is easier to reuse, since it knows nothing about View. (follows from the first paragraph)
  3. In MVVM, the state problem is solved, but not completely. In this example, we have property in the ViewModel, from where View takes the data. When we make a request to the network, the data will be stored in the property, and View will receive valid data when subscribing (and you don’t even have to dance with a tambourine). We can also make the property persistent, which allows us to save them in the event of the death of the process.


MVI



Define Actions, SideEffects and State







 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 )
      
      





Next comes Reducer







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





and EffectHandler for handling 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)) } } } }
      
      





What we have:







  1. In MVI, we also do not need to create a bunch of contracts for View. You only need to define the render (State) function.
  2. To reuse this, unfortunately, is not so simple, since we have State, which can be quite specific.
  3. In MVI, we have a certain state that we can change centrally through the reduce function. Thanks to this, we can track state changes. For example, write all changes to the log. Then we can read the last state if the application crashes. Plus State can be persistent, which allows you to handle the death of the process.


Total



MVVM solves the problem of process death. But, unfortunately, the condition here is still uncertain and cannot change centrally. This, of course, is a minus, but the situation still became clearly better than in MVP. MVI solves the state problem, but the approach itself can be a bit complicated. Plus, there is a problem with the UI, since the current UI toolkit in android is bad. In MVVM, we update the UI in pieces, and in MVI we strive to update it as a whole. Therefore, for an imperative ui, MVVM will behave better. If you want to use MVI, then I advise you to get acquainted with the theory of virtual / incremental DOM and libraries for android: litho, anvil, jetpack compose (you have to wait). Or you can take diffs with your hands.







Based on all the data above, I would advise choosing between MVVM and MVI when designing an application. So you get a more modern and convenient approach (especially in the realities of Android).







Libraries that can help implement these approaches:

MVVM - https://github.com/Miha-x64/Lychee

MVI - https://github.com/egroden/mvico , https://github.com/badoo/MVICore , https://github.com/arkivanov/MVIDroid







Thank you all for your attention!








All Articles