Olá pessoal! Hoje eu gostaria de falar sobre a arquitetura dos aplicativos Android.
Na verdade, eu realmente não gosto de relatórios e artigos sobre esse tópico, mas recentemente cheguei à conclusão com a qual gostaria de compartilhar.
Quando eu comecei a me familiarizar com arquiteturas, meus olhos se voltaram para o MVP. Gostei da simplicidade e da disponibilidade de uma enorme quantidade de materiais de treinamento.
Mas com o tempo, comecei a perceber que algo estava errado. Havia um sentimento de que é possível melhor.
Quase todas as implementações que vi foram assim: temos uma classe abstrata da qual herdamos todos os nossos apresentadores.
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) } } } }
Também criamos uma interface de visualização para cada tela, com a qual o apresentador funcionará
interface MovieView : MvpView { fun showMovies(movies: List<Movie>) fun showError() }
Vejamos as desvantagens dessa abordagem:
- Você precisa criar uma interface de exibição para cada tela. Em grandes projetos, teremos muitos códigos e arquivos extras que dificultam a navegação por pacotes.
- É difícil reutilizar o Presenter, pois está vinculado à Visualização e pode ter métodos específicos.
- Uma condição específica está ausente. Imagine que estamos fazendo um pedido à rede e, neste momento, nossa atividade está morrendo e uma nova está sendo criada. Os dados vieram quando o View ainda não está vinculado ao Presenter. Isso levanta a questão de como mostrar esses dados quando a Visualização está vinculada ao Presenter? Resposta: apenas muletas. Moxy, por exemplo, tem um ViewState que armazena a lista ViewCommand. Esta solução funciona, mas parece-me que arrastar o código para salvar o estado de exibição é supérfluo (o multidex está muito mais próximo do que você pensa. Além disso, o assembly começará a processar anotações, o que o tornará mais longo. Sim, você dirá que agora temos kapt incremental, mas certas condições são necessárias para sua operação). Além disso, o ViewCommand não é Parcelable ou Serializable, o que significa que não podemos salvá-los em caso de morte do processo. É importante ter um estado persistente para não perder nada. Além disso, a ausência de um determinado estado não permite que ele seja alterado centralmente, e isso pode levar a erros de reprodução difíceis.
Vamos ver se esses problemas foram resolvidos em outras arquiteturas.
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 } } } }
Vamos examinar os pontos mencionados acima:
- No MVVM, o VIew não possui mais uma interface, pois simplesmente assina campos observáveis no ViewModel.
- O ViewModel é mais fácil de reutilizar porque não sabe nada sobre o View. (segue do primeiro parágrafo)
- No MVVM, o problema de estado é resolvido, mas não completamente. Neste exemplo, temos propriedade no ViewModel, de onde o View obtém os dados. Quando fazemos uma solicitação para a rede, os dados são salvos na propriedade e o View recebe dados válidos ao se inscrever (e você nem precisa dançar com um pandeiro). Também podemos manter a propriedade persistente, o que permitirá que eles sejam preservados em caso de morte do processo.
MVI
Definir ações, efeitos colaterais e 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 )
Em seguida vem o redutor
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() } }
e EffectHandler para manipular 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)) } } } }
O que temos:
- No MVI, também não precisamos criar vários contratos para o View. Você só precisa definir a função render (State).
- Reutilizar isso, infelizmente, não é tão simples, pois temos Estado, que pode ser bastante específico.
- No MVI, temos um certo estado que podemos mudar centralmente através da função de redução. Graças a isso, podemos rastrear alterações de estado. Por exemplo, escreva todas as alterações no log. Em seguida, podemos ler o último estado se o aplicativo falhar. O estado Plus pode ser persistente, o que permite que você lide com a morte do processo.
Sumário
MVVM resolve o problema de morte do processo. Mas, infelizmente, o estado aqui ainda é incerto e não pode mudar centralmente. É claro que isso é um sinal de menos, mas a situação ainda se tornou claramente melhor do que no MVP. O MVI resolve o problema de estado, mas a abordagem em si pode ser um pouco complicada. Além disso, há um problema com a interface do usuário, pois o kit de ferramentas da interface do usuário atual no Android é ruim. No MVVM, atualizamos a interface do usuário em partes e no MVI nos esforçamos para atualizá-la como um todo. Portanto, para uma interface de usuário imperativa, o MVVM se comportará melhor. Se você quiser usar o MVI, recomendo que você se familiarize com a teoria do DOM virtual / incremental e das bibliotecas para android: litho, bigorna, jetpack compor (você precisa esperar). Ou você pode tirar diffs com as mãos.
Com base em todos os dados acima, aconselho que você escolha entre MVVM e MVI ao projetar um aplicativo. Assim, você obtém uma abordagem mais moderna e conveniente (especialmente nas realidades do Android).
Bibliotecas que podem ajudar a implementar essas abordagens:
MVVM - https://github.com/Miha-x64/Lychee
MVI - https://github.com/egroden/mvico , https://github.com/badoo/MVICore , https://github.com/arkivanov/MVIDroid
Obrigado a todos pela atenção!