Modularização Android Parte 4.2 — Refatorando

MVVM, Koin, Rx, Room, Databinding e um pouco mais… *Coroutines

Iago Mendes Fucolo
Android Dev BR
8 min readApr 15, 2020

--

Continuando…

Já refatoramos a primeira parte do nosso projeto, agora vamos seguir em frente com as mudanças.

Nos primeiros artigos usamos Rx para lidar com nossas chamadas assíncronas, mas agora vamos utilizar Coroutines, somente para que você possa enxergar como uma e a outra são utilizadas (no fim a preferência sempre é do desenvolvedor de qual usar). Vamos também tentar de alguma forma deixar nosso código um pouco melhor.

First things first:

Antes de mais nada, vamos atualizar algumas dependências do projeto. Para que a gente não perca muito tempo com isso, vou somente citar o que foi adicionado, e deixar os links dos arquivos que vocês devem atualizar.

Dependências adicionados a dependencies.gradle: coroutines-core, coroutines-android, material e room-ktx.

Dependências atualizadas a dependencies.gradle: roomVersion, retrofitVersion, lifeCycleVersion e kotlinVersion.

Build.gradle: dependências adicionadas por módulo:

  • app: dependencies.coroutinesCore, dependencies.coroutinesAndroid e dependencies.material
  • domain: dependencies.coroutinesCore, dependencies.coroutinesAndroid
  • data: dependencies.coroutinesCore, dependencies.coroutinesAndroid e dependencies.roomKtx

Domain

Vamos começar as mudanças por esse módulo e a primeira coisa que vamos adicionar é o package response, onde nele irá conter 2 classes genéricas para utilizarmos como retornos do repository e de dados doremote.

Primeiro, temos a sealed class ResulRemote, que será usada para os retornos de dados que vem do servidor. Como vocês podem ver temos 2 tipos de retorno: Success e ErrorResponse.

Success: será usado quando a nossa chamada para o servidor ocorrer como esperada. Somente vamos ter que passar em T o tipo de retorno esperado.

ErrorResponse: aqui temos uma sealed class dentro do ResultRemote. Qual a razão para isso? Queremos diferenciar os tipos de erros e considerá-los todos como tipos de ErrorResponse. Para ErrorResponse estender ResultRemote, precisamos passar um tipo e, por isso, utilizamos tipo Nothing, pois não esperamos que retorne nenhum objeto em caso de erro. E, para finalizar, temos que implementar throwable nos tipos de erros que temos.

Segundo, temos a sealed class ResulRequired, que será utilizada nos retornos dos repositórios:

Não entraremos a fundo aqui, pois a explicação de ResultRemote já contempla essa classe também.

AndroidJobsRepository: vamos atualizar essa interface para que ela utilize nossos novos recursos:

A fun getJobs antes retornava um Single<List<AndroidJob>>, mas agora, retorna um Flow com o ResultRequired, tendo o tipo List<AndroidJob>. E, também, adicionamos um método add, que servirá para adicionar jobs em algum lugar (veremos em AndroidJobsRepositoryImpl).

Antes de mais nada, se você ainda não ouviu falar de coroutines Flow, você deve estar se perguntando o que isso significa, certo? Certo.

Definição de FLOW na documentação traduzida literalmente:

Um fluxo de dados assíncrono frio que emite sequencialmente valores
e é concluído normalmente ou com uma exceção.

Por enquanto essa definição será suficiente para seguirmos (mais a frente quando chegarmos no módulo data, vamos aprofundar um pouco mais).

GetJobsUseCases:

Antes tínhamos somente a implementação de GetJobsUseCases, mas isso não é muito legal de fazer, porque quem utiliza esse UseCase não deve saber da sua implementação. Por isso, separamos em interface e implementação, e assim quem quiser algo do UseCase deverá utilizar a interface GetJobsUseCases, que está implementada em GetJobsUseCasesImpl.

Primeiro, vamos falar da intercace GetJobsUseCases. Como é algo novo, vamos falar dos dois métodos e de ResultJob.

getJobs: retorna um flow com ResultJobs (já iremos falar dessa classe).

addJob: simples função para adicionarmos um novo AndroidJob.

ResultJobs: uma sealed class contendo os possíveis retornos de getJobs, que temos 3 tipos:

  • Jobs: contém a lista de jobs.
  • NoJobs: quando não existem jobs para retornar.
  • Error: quando algo deu errado.

Agora vamos para a parte final do UseCase: a implementação de cada método:

Vamos analisar a implementação de getJobs: como o repositório retorna um Flow, precisamos usar map para pegar somente o valor que vem dentro do Flow, assim vamos mapear o ResultRequired que vem do repositório em ResultJobs.

Quando vir do repository ResultRequired.Success, vamos verificar se a lista for vazia e, em caso positivo, retornamos ResultJobs.NoJobs. Caso contrário, retornamos ResultRequired.Jobs com a lista de AndrodiJobs. Quando o repositório retornar ResultRequired.Error, vamos printar o error e retornar ResultJobsView.Error.

E por fim, mas não menos importante, o método addJob, que simplesmente chama o método add do repositório:

Não esqueça de atualizar a DomainModule, segue o LINK.

Data: Remote

No módulo data vamos começar pelo package remote.

A primeira mudança será na interface ServerApi, onde temos o método que retorna os jobs do servidor. A mudança foi bem simples:

Antes o retorno era um Single<JobsPayload>, mas agora retornamos somente o JobsPayload. Isso acontece por que estamos utilizando suspend fun, e isso só é permitido com o retrofit a partir da versão 2.6.0.

A função de suspensão é uma função que pode ser iniciada, pausada e retomada (e pausar e retomar … se desejado repetidamente) e depois terminar.. fonte.

Em seguida vamos atualizar a interface RemoteDataSource, onde trocaremos o retorno da função getJobs, para retornar um ResultRemote<JobsPayload>, e ela também será uma suspend function.

ps: colocar RemoteDataSource junto com RemoteDataSourceImpl

Já na implementação, temos o seguinte código:

Aqui, estamos usando um try catch, para, caso ocorra alguma exceção, podermos tratar e retornar o erro sem quebrar o fluxo.

Primeiro, fazemos jobsPayload receber o resultado de fetchJobs, que será um objeto de JobsPayload e, se tudo ocorrer bem, vamos passar esse payload dentro de ResultRemote.Success, como podemos ver acima.

Caso ocorra algum problema durante a chamada, o catch é executado. Para isso, criamos uma extension para lidar com erro, somente a título de exemplificação:

É uma extension que vai traduzir o Throwable em algum tipo de erro que fique mais claro. O exemplo acima verifica se é uma HttpException e se o código de erro é 401 ou 403 - que, nesse caso, significa token expirado -, então retorna ResultRemote.ErrorResponse.TokenExpired. Em todos os outros casos retorna ResultRemote.ErrorResponse.Unknown.

Acho interessante salientar que você pode implementar diferentes casos de erro e retornar o que você achar necessário. Aqui usamos exemplos somente para você enxergar as possibilidades.

Data: Local

A primeira coisa que vamos mudar aqui será o nosso JobsDao. Primeiro, vamos adicionar o método insert para inserir jobs no banco de dados. E também vamos atualizar o método getJobs, para retornar um Flow contendo a lista de AndroidJobs no banco dados:

Como falamos anteriormente, agora chegou a hora de entendermos um pouco melhor coroutines Flow:

Toda vez que adicionamos, deletamos ou atualizamos algum dado no banco de dados, gostaríamos de manter nossa UI atualizada, certo? Usando o Flow no return da query getJobs, toda vez que algo for modificado no banco de dados essa query vai emitir um novo Flow. Com isso quem assinar esse Flow sempre terá a última versão dos dados.

Para mais detalhes:

Seguindo adiante, já que o Flow está emitindo seus dados, precisamos atualizar, JobsCacheDataSource para emitir o Flow e também para adicionar novos AndroidJobs, ficando assim a nossa interface:

ps: movemos essa interface para ficar no mesmo arquivo de sua implementação

Veja agora como ficou a implementação:

Um detalhe importante, que talvez você tenha notado, é que agora esse source só aceita receber dados que ele mesmo pode usar, ou seja, só recebe dados da cache, assim como também só emite dados da cache.

O único método que vamos comentar aqui é que getJobs do source retorna o getJobs de JobsDao, que retorna nada mais, nada menos, que a lista de AndroidJobsCache.

Com o Remote e Cache contemplados, podemos seguir em frente com a atualização/refatorção da implementação de AndroidJobsRepositoryImpl.

Data: AndroidJobsRepositoryImpl

Vamos percorrer cada método falando um pouco de cada um.

getJobsRemote:

Como já sabemos, esse método serve para pegarmos dados do backend e devido ao nosso refactor precisamos fazer algumas mudanças para que ele continue funcionando normalmente, assim como os outros:

Antes, retornávamos uma lista de AndrodJobs, mas agora vamos utilizar o ResulRequired com a lista de AndrodJobs.

Não tínhamos nenhum tratamento de erro caso algo desse errado nessa chamada e, como mudamos o retorno de remoteDataSource.getJobs() para ResultRemote, agora podemos também tratar os erros.

Caso tudo aconteça bem, vamos receber um Result.Success contendo JobsPayload. Em seguida, vamos mapear o resultado do backend para uma List<AndroidJob>, e, assim, temos o que queremos retornar no ResultRequired. Mas, antes de retornar o resultado, vamos salvar essa nova lista no banco de dados, e, para isso, vamos mapear a lista de AndroidJobs para uma lista de AndroidJobCache. E, por fim, vamos retornar Result.Success contendo a lista de AndroidJob.

Quando tivermos um ResultRemote.ErrorResponse do backend vamos simplesmente mapeá-lo para ResultRequired.Error passando o throwable.

add:

Esse método serve única e exclusivamente para adicionarmos um AndroidJob no banco de dados.

Criamos um AndroidJob, mapeamos ele para a cache e, então, o salvamos e nada mais.

getJobs:

E, por fim e mais importante, o método que retorna os AndroidJobs.

Primeiro, vamos pegar os AndroidJobs da cache/banco de dados. Caso a lista esteja vazia, vamos chamar o getJobsRemotes, que tem o mesmo tipo de retorno ResultRequired<List<AndroidJob>>, dessa função. Caso a lista não esteja vazia, vamos mapeá-la para o AndroidJob e retornar o Result.Success contendo a lista.

App

Depois de uma longa jornada, finalmente chegamos a última parte, atualizar o módulo app.

AndroidJobListViewModel

Vamos começar pelo ViewModel, que sofreu alterações consideráveis.

Primeiro criamos uma sealed class para nos comunicarmos com a view, com os seguintes estados:

E, em seguid, vamos modificar o LiveData, e usar Event com o estado, como no artigo anterior:

Vamos setar o LiveData recebendo o MutableLiveData, ambos com o tipo Event<ViewJobsStates>.

Agora vamos explorar os métodos:

onTryAgainRequired:

Somente chama o método getJobs.

add:

Aqui vamos chamar o UseCase para adicionar um novo AndroidJob.

Poucas linhas, mas muitas coisas a se dizer:

viewModelScope.launch:

O viewModelScope é definido para cada ViewModel na aplicação, e toda coroutine que é iniciada é automaticamente cancelada quando o ViewModel é destruido, de modo a evitar o consumo de recursos desnecessários. doc

Dispatchers.IO

Referência.

Traduzindo literalmente: Utilizar em operações relacionadas a data base, ler, escrever e chamadas de backend. (Para mais detalhes, ler o artigo referência acima).

E, por fim, chamamos o jobsUseCase.addJob, que precisa fazer operações de escrita no banco dados. Caso esse método não esteja dentro do launch, você ira receber o seguinte crash:

Cannot access database on the main thread since it may potentially lock the UI for a long period of time.viewModelScope.

E já que agora sabemos o como funciona o viewModelScope.launch(Dispatchers.IO), vamos para o ultimo método do ViewModel.

getJobs:

Como já estamos cansados de saber, ele vai fazer a chamada para o UseCase pedindo AndroidJobs e, então, mapear o resultado, para que seja emitido pelo LiveData.

Vamos direto para o collect, nesse método.

Como jobsUseCase.getJobs retorna um Flow, precisamos literalmente coletar o resultado, e é isso que collect faz: ele nos entrega o objeto que esperamos desse Flow, que, nesse caso, é ResultJobs.

Para cada resultado, definimos um ViewJobsStates e, por fim, emitimos esse state no _viewJobsStatesLiveData.

AndroidJobsListActivity

Agora, precisamos refatorar a nossa activity para refletir as mudanças que fizemos no ViewModel e também refatorar outros métodos.

hideAll: Esconde todas as views da tela.

E depois um método para cada tipo de estado da view:

E, para fechar, temos o método setupViewModel:

Toda vez que tiver uma emissão de state, este método vai esconder todas as views, e depois, de acordo com o state, vai chamar o método correspondente ao estado.

Chegamos ao fim, mas quem sabe vamos falar de teste no próximo?

commit com as modificações desse artigo: commit.

Branch da refatoração: coroutine

Ficou com alguma dúvida?

Manda um e-mail que terei o prazer de responder.

iagofucolo@gmail.com

follow me twitter, linkedin.

--

--

Iago Mendes Fucolo
Android Dev BR

Android Engineer @NewMotion, Writer, Ex almost footballer, and Brazilian.