Se você não escolher a arquitetura certa para o seu Android projeto, você terá dificuldade em mantê-lo à medida que sua base de código crescer e sua equipe se expandir.
c corporação e diferenças corporativas
Este não é apenas um tutorial do Android MVVM. Neste artigo, vamos combinar MVVM (Model-View-ViewModel ou às vezes estilizado “o padrão ViewModel”) com Arquitetura Limpa . Veremos como essa arquitetura pode ser usada para escrever código desacoplado, testável e sustentável.
O MVVM separa sua visão (ou seja, Activity
se Fragment
s) de sua lógica de negócios. MVVM é suficiente para pequenos projetos, mas quando sua base de código se torna enorme, seus ViewModel
s começam a inchar. Separar responsabilidades torna-se difícil.
MVVM com Clean Architecture é muito bom nesses casos. Ele vai um passo além na separação das responsabilidades de sua base de código. Ele abstrai claramente a lógica das ações que podem ser executadas em seu aplicativo.
Nota: Você também pode combinar Clean Architecture com a arquitetura model-view-presenter (MVP). Mas desde Componentes de arquitetura Android já fornece um ViewModel
integrado | classe, vamos com MVVM sobre MVP - nenhuma estrutura MVVM necessária!
Nosso fluxo de dados será semelhante a este:
Nossa lógica de negócios é completamente desacoplada de nossa IU. Isso torna nosso código muito fácil de manter e testar.
O exemplo que veremos é bastante simples. Ele permite aos usuários criar novas postagens e ver uma lista de postagens criadas por eles. Não estou usando nenhuma biblioteca de terceiros (como Dagger, RxJava, etc.) neste exemplo por uma questão de simplicidade.
O código é dividido em três camadas separadas:
Entraremos em mais detalhes sobre cada camada abaixo. Por enquanto, nossa estrutura de pacote resultante se parece com isto:
Mesmo dentro da arquitetura do aplicativo Android que estamos usando, há muitas maneiras de estruturar sua hierarquia de arquivos / pastas. Gosto de agrupar arquivos de projeto com base em recursos. Acho que é limpo e conciso. Você é livre para escolher a estrutura de projeto mais adequada para você.
Isso inclui nossos Activity
s, Fragment
s e ViewModel
s. An Activity
deve ser o mais burro possível. Nunca coloque sua lógica de negócios em Activity
s.
An Activity
falará com um ViewModel
e um ViewModel
falará com a camada de domínio para realizar ações. A ViewModel
nunca fala diretamente com a camada de dados.
Aqui estamos passando um UseCaseHandler
e dois UseCase
s para o nosso ViewModel
. Entraremos em mais detalhes em breve, mas nesta arquitetura, um UseCase
é uma ação que define como um ViewModel
interage com a camada de dados.
Veja como nosso Código Kotlin parece:
class PostListViewModel( val useCaseHandler: UseCaseHandler, val getPosts: GetPosts, val savePost: SavePost): ViewModel() { fun getAllPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) { val requestValue = GetPosts.RequestValues(userId) useCaseHandler.execute(getPosts, requestValue, object : UseCase.UseCaseCallback { override fun onSuccess(response: GetPosts.ResponseValue) { callback.onPostsLoaded(response.posts) } override fun onError(t: Throwable) { callback.onError(t) } }) } fun savePost(post: Post, callback: PostDataSource.SaveTaskCallback) { val requestValues = SavePost.RequestValues(post) useCaseHandler.execute(savePost, requestValues, object : UseCase.UseCaseCallback { override fun onSuccess(response: SavePost.ResponseValue) { callback.onSaveSuccess() } override fun onError(t: Throwable) { callback.onError(t) } }) } }
A camada de domínio contém todos os casos de uso de seu aplicativo. Neste exemplo, temos UseCase
, uma classe abstrata. Todos os nossos UseCase
s estenderão esta classe.
abstract class UseCase { var requestValues: Q? = null var useCaseCallback: UseCaseCallback? = null internal fun run() { executeUseCase(requestValues) } protected abstract fun executeUseCase(requestValues: Q?) /** * Data passed to a request. */ interface RequestValues /** * Data received from a request. */ interface ResponseValue interface UseCaseCallback { fun onSuccess(response: R) fun onError(t: Throwable) } }
E UseCaseHandler
lida com a execução de um UseCase
. Nunca devemos bloquear a IU quando buscamos dados do banco de dados ou de nosso servidor remoto. Este é o lugar onde decidimos executar nosso UseCase
em um thread de segundo plano e receber a resposta no thread principal.
class UseCaseHandler(private val mUseCaseScheduler: UseCaseScheduler) { fun execute( useCase: UseCase, values: T, callback: UseCase.UseCaseCallback) { useCase.requestValues = values useCase.useCaseCallback = UiCallbackWrapper(callback, this) mUseCaseScheduler.execute(Runnable { useCase.run() }) } private fun notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback) { mUseCaseScheduler.notifyResponse(response, useCaseCallback) } private fun notifyError( useCaseCallback: UseCase.UseCaseCallback, t: Throwable) { mUseCaseScheduler.onError(useCaseCallback, t) } private class UiCallbackWrapper( private val mCallback: UseCase.UseCaseCallback, private val mUseCaseHandler: UseCaseHandler) : UseCase.UseCaseCallback { override fun onSuccess(response: V) { mUseCaseHandler.notifyResponse(response, mCallback) } override fun onError(t: Throwable) { mUseCaseHandler.notifyError(mCallback, t) } } companion object { private var INSTANCE: UseCaseHandler? = null fun getInstance(): UseCaseHandler { if (INSTANCE == null) { INSTANCE = UseCaseHandler(UseCaseThreadPoolScheduler()) } return INSTANCE!! } } }
Como o próprio nome indica, o GetPosts
UseCase
é responsável por obter todas as postagens de um usuário.
class GetPosts(private val mDataSource: PostDataSource) : UseCase() { protected override fun executeUseCase(requestValues: GetPosts.RequestValues?) { mDataSource.getPosts(requestValues?.userId ?: -1, object : PostDataSource.LoadPostsCallback { override fun onPostsLoaded(posts: List) { val responseValue = ResponseValue(posts) useCaseCallback?.onSuccess(responseValue) } override fun onError(t: Throwable) { // Never use generic exceptions. Create proper exceptions. Since // our use case is different we will go with generic throwable useCaseCallback?.onError(Throwable('Data not found')) } }) } class RequestValues(val userId: Int) : UseCase.RequestValues class ResponseValue(val posts: List) : UseCase.ResponseValue }
O objetivo do UseCase
s é ser um mediador entre seus ViewModel
se Repository
s.
Digamos que no futuro você decida adicionar um recurso de “editar postagem”. Tudo o que você precisa fazer é adicionar um novo EditPost
UseCase
e todo o seu código será completamente separado e desacoplado de outros UseCase
s. Todos nós já vimos isso muitas vezes: novos recursos são introduzidos e eles inadvertidamente quebram algo no código preexistente. Criando um UseCase
separado | ajuda imensamente a evitar isso.
classe javascript não está definida
Claro, você não pode eliminar essa possibilidade 100 por cento, mas com certeza pode minimizá-la. Isso é o que separa a Arquitetura Limpa de outros padrões: o código é tão desacoplado que você pode tratar cada camada como uma caixa preta.
Isso contém todos os repositórios que a camada de domínio pode usar. Esta camada expõe uma API de fonte de dados para classes externas:
interface PostDataSource { interface LoadPostsCallback { fun onPostsLoaded(posts: List) fun onError(t: Throwable) } interface SaveTaskCallback { fun onSaveSuccess() fun onError(t: Throwable) } fun getPosts(userId: Int, callback: LoadPostsCallback) fun savePost(post: Post) }
PostDataRepository
implementa PostDataSource
. Ele decide se buscamos dados de um banco de dados local ou de um servidor remoto.
class PostDataRepository private constructor( private val localDataSource: PostDataSource, private val remoteDataSource: PostDataSource): PostDataSource { companion object { private var INSTANCE: PostDataRepository? = null fun getInstance(localDataSource: PostDataSource, remoteDataSource: PostDataSource): PostDataRepository { if (INSTANCE == null) { INSTANCE = PostDataRepository(localDataSource, remoteDataSource) } return INSTANCE!! } } var isCacheDirty = false override fun getPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) { if (isCacheDirty) { getPostsFromServer(userId, callback) } else { localDataSource.getPosts(userId, object : PostDataSource.LoadPostsCallback { override fun onPostsLoaded(posts: List) { refreshCache() callback.onPostsLoaded(posts) } override fun onError(t: Throwable) { getPostsFromServer(userId, callback) } }) } } override fun savePost(post: Post) { localDataSource.savePost(post) remoteDataSource.savePost(post) } private fun getPostsFromServer(userId: Int, callback: PostDataSource.LoadPostsCallback) { remoteDataSource.getPosts(userId, object : PostDataSource.LoadPostsCallback { override fun onPostsLoaded(posts: List) { refreshCache() refreshLocalDataSource(posts) callback.onPostsLoaded(posts) } override fun onError(t: Throwable) { callback.onError(t) } }) } private fun refreshLocalDataSource(posts: List) { posts.forEach { localDataSource.savePost(it) } } private fun refreshCache() { isCacheDirty = false } }
O código é basicamente autoexplicativo. Esta classe possui duas variáveis, localDataSource
e remoteDataSource
. Seu tipo é PostDataSource
, então não nos importamos como eles são realmente implementados nos bastidores.
Em minha experiência pessoal, esta arquitetura provou ser inestimável. Em um de meus aplicativos, comecei com o Firebase no back-end, o que é ótimo para criar seu aplicativo rapidamente. Eu sabia que eventualmente teria que mudar para meu próprio servidor.
Quando eu fiz, tudo que tive que fazer foi mudar a implementação em RemoteDataSource
. Eu não tive que tocar em nenhuma outra classe, mesmo depois de uma mudança tão grande. Essa é a vantagem do código desacoplado. Alterar qualquer classe não deve afetar outras partes do seu código.
Algumas das aulas extras que temos são:
interface UseCaseScheduler { fun execute(runnable: Runnable) fun notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback) fun onError( useCaseCallback: UseCase.UseCaseCallback, t: Throwable) } class UseCaseThreadPoolScheduler : UseCaseScheduler { val POOL_SIZE = 2 val MAX_POOL_SIZE = 4 val TIMEOUT = 30 private val mHandler = Handler() internal var mThreadPoolExecutor: ThreadPoolExecutor init { mThreadPoolExecutor = ThreadPoolExecutor(POOL_SIZE, MAX_POOL_SIZE, TIMEOUT.toLong(), TimeUnit.SECONDS, ArrayBlockingQueue(POOL_SIZE)) } override fun execute(runnable: Runnable) { mThreadPoolExecutor.execute(runnable) } override fun notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback) { mHandler.post { useCaseCallback.onSuccess(response) } } override fun onError( useCaseCallback: UseCase.UseCaseCallback, t: Throwable) { mHandler.post { useCaseCallback.onError(t) } } }
UseCaseThreadPoolScheduler
é responsável por executar tarefas de forma assíncrona usando ThreadPoolExecuter
.
class ViewModelFactory : ViewModelProvider.Factory { override fun create(modelClass: Class): T { if (modelClass == PostListViewModel::class.java) { return PostListViewModel( Injection.provideUseCaseHandler() , Injection.provideGetPosts(), Injection.provideSavePost()) as T } throw IllegalArgumentException('unknown model class $modelClass') } companion object { private var INSTANCE: ViewModelFactory? = null fun getInstance(): ViewModelFactory { if (INSTANCE == null) { INSTANCE = ViewModelFactory() } return INSTANCE!! } } }
Este é o nosso ViewModelFactory
. Você tem que criar isso para passar argumentos em seu ViewModel
construtor.
Vou explicar a injeção de dependência com um exemplo. Se você olhar para nosso PostDataRepository
classe, tem duas dependências, LocalDataSource
e RemoteDataSource
. Usamos o Injection
classe para fornecer essas dependências ao PostDataRepository
classe.
A dependência de injeção tem duas vantagens principais. Uma é que você controla a instanciação de objetos a partir de um local central, em vez de espalhá-la por toda a base de código. Outra é que isso nos ajudará a escrever testes de unidade para PostDataRepository
porque agora podemos apenas passar versões simuladas de LocalDataSource
e RemoteDataSource
para PostDataRepository
construtor em vez de valores reais.
object Injection { fun providePostDataRepository(): PostDataRepository { return PostDataRepository.getInstance(provideLocalDataSource(), provideRemoteDataSource()) } fun provideViewModelFactory() = ViewModelFactory.getInstance() fun provideLocalDataSource(): PostDataSource = LocalDataSource.getInstance() fun provideRemoteDataSource(): PostDataSource = RemoteDataSource.getInstance() fun provideGetPosts() = GetPosts(providePostDataRepository()) fun provideSavePost() = SavePost(providePostDataRepository()) fun provideUseCaseHandler() = UseCaseHandler.getInstance() }
Nota: Eu prefiro usar o Dagger 2 para injeção de dependência em projetos complexos. Mas com sua curva de aprendizado extremamente íngreme, está além do escopo deste artigo. Então, se você estiver interessado em se aprofundar, eu recomendo fortemente Introdução de Hari Vignesh Jayapalan a Dagger 2 .
Nosso objetivo com este projeto era entender MVVM com Arquitetura Limpa, então pulamos algumas coisas que você pode tentar melhorar ainda mais:
Esta é uma das melhores e mais escalonáveis arquiteturas para aplicativos Android. Espero que tenha gostado deste artigo e estou ansioso para saber como você usou essa abordagem em seus próprios aplicativos!
Relacionado: Xamarin Forms, MVVMCross e SkiaSharp: The Holy Trinity of Cross-Platform App DevelopmentA arquitetura do Android é a maneira como você estrutura o código do seu projeto Android para que ele seja escalonável e fácil de manter. Os desenvolvedores gastam mais tempo mantendo um projeto do que inicialmente construindo-o, portanto, faz sentido seguir um padrão arquitetônico adequado.
No Android, MVC se refere ao padrão padrão em que uma Activity atua como um controlador e os arquivos XML são visualizações. O MVVM trata as classes Activity e arquivos XML como visualizações, e as classes ViewModel são onde você escreve sua lógica de negócios. Ele separa completamente a IU de um aplicativo de sua lógica.
No MVP, o apresentador conhece a visão e a visão conhece o apresentador. Eles interagem entre si por meio de uma interface. No MVVM, apenas a visão conhece o modelo de visão. O modelo de visualização não tem ideia sobre a visualização.
Um é a separação de interesses, ou seja, sua lógica de negócios, IU e modelos de dados devem estar em lugares diferentes. Outra é a dissociação do código: cada pedaço de código deve atuar como uma caixa preta para que qualquer alteração em uma classe não tenha nenhum efeito em outra parte de sua base de código.
A 'Arquitetura Limpa' de Robert C. Martin é um padrão que permite dividir sua interação com os dados em entidades mais simples chamadas de 'casos de uso'. É ótimo para escrever código desacoplado.
A maioria dos aplicativos salva e recupera dados do armazenamento local ou de um servidor remoto. Repositórios Android são classes que decidem se os dados devem vir de um servidor ou armazenamento local, desacoplando sua lógica de armazenamento de classes externas.