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:
Let's see if these problems are solved in other architectures.
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:
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:
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!