Modularização Android Parte 2

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

Iago Mendes Fucolo
Jun 6 · 7 min read

No primeiro artigo da série, fizemos o setup inicial do projeto e criamos o nosso module domain. Caso não tenha visto a primeira parte, abaixo você pode ir para o artigo anterior.

Data Module:

Todos os projetos Android possuem dados, os quais precisam ser fornecidos de algum lugar, e é justamente isso que o module data faz para nós. Esses dados podem vir de qualquer lugar, como de alguma api ou database.

Quando a domain pede algum dado, ela não sabe de onde eles são fornecidos, pois isso é responsabilidade do modulo data.

Conteúdo do data:

  • Api: temos aqui todos os endpoints que vamos utilizar para requisitar dados do backend.
  • Model: aqui ficam as entidades que vêm do backend ou da cache, ou seja, o dado puro, que só é utilizado no modulo data.
  • Mapper: onde vamos mapear nossos models para as entidades exigidas pela domain.
  • RepositoryImpl: aqui implementamos a interface repository da domain e é onde vamos decidir de qual lugar vamos pegar os dados se da cache ou do backend.
  • CacheDataSource: interface de comunicação que é implementada na nossa cache para pegar os dados localmente.
  • CacheDataSourceImpl: aqui vamos salvar os dados na cache (no nosso caso, room database) e fornecer eles já mapeados.
  • RemoteDataSource: interface de comunicação que é implementada no remote para pegar dados do backend.
  • RemoteDataSourceImpl: aqui vamos chamar a nossa server api, para pegar os dados do backend e enviar para quem solicitou ja mapeados.

Diagrama de fluxo do modulo data:

A domain solicita algum dado para o seu repository, que está implementado no RepositoryImpl, que então decide de onde vai buscar os dados solicitados. Primeiro, chamamos a nossa cache para verificar se temos algum dado para retornar e, caso não tenha, chamamos o remote e salvamos esses dados na cache e, então, retornamos os dados requeridos para a domain.

Agora que já temos uma ideia de como funciona o nosso modulo data, vamos criá-lo. O processo é praticamente o mesmo explicado no artigo anterior, sendo que a única diferença é que, ao invés de selecionar um java Library, vamos selecionar Android Library.

Vamos começar o desenvolvimento do módulo:

A primeira coisa que vamos fazer é adicionar as dependências necessárias ao nosso arquivo dependencies.gradle, para o modulo data.

Como vamos ter chamadas para o backend, vamos utilizar Retrofit e, para a nossa cache, vamos utilizar Room.

A ideia não é aprofundar conhecimentos sobre as bibliotecas, por isso vamos explicar brevemente ao longo do desenvolvimento e, caso tenha alguma duvida é só entrar nos links destas (ao longo do artigo, temos alguns links de como utilizá-las). Mas, caso ainda tenha dúvidas, sinta-se à vontade para entrar em contato.

Ficarão assim nossas dependências atualizadas:

Obs: Se quiser utilizar versões diferentes ou outras libs, fique à vontade, pois o conceito é independente das libs utilizadas.

E agora vamos chamar nossas libs no gradle do módulo data:

É possível que você não tenha reparado, mas nas dependências colocamos 3 propriedades: minSdk, targetSdk e compileSdk, que estão sendo utilizadas no arquivo gradle do data.

Esse module necessita da domain, pois é aqui que vamos implementar os repositórios para retornar os dados que estamos buscando.

Agora, com as dependências configuradas, podemos começar a desenvolver nosso module.

Como já vimos anteriormente, vamos mostrar uma lista de AndroidJobs no final:

Obs: nosso projeto mostrara uma lista de jobs Android e vamos utilizar a seguinte api: http://demo8470178.mockable.io/android-jobs

{
"jobs": [
{
"title": "Android developer",
"native" : true,
"required_experience_years": 3,
"country": "Brazil"
},
...
]
}

CacheData:

Vamos começar implementando a nossa cache e deixando ela preparada para salvar nossos dados:

  • Model: aqui vamos criar a entidade que vamos utilizar no nosso database.

Essa é nossa entidade que será utilizada para o nosso database.

  • DataBase: precisamos criar o nosso banco de dados e também, de alguma forma, interagir com ele. Por isso, qui temos duas classes:

JobsDao: nossa interface de interação com o banco de dados, como a própria expressão “DAO” já deixa claro (Data Access Object):

Temos 4 métodos de interação com o banco de dados:

  • getJobs: vai retornar uma lista de AndroidJobsCache do banco jobs (que setamos anteriormente Entity(“tableName = jobs”)).
  • inserAll: insere uma lista de AndroidJobs no banco jobs.
  • deleteAll: deleta todos os dados do banco jobs.
  • updateData: deleta os dados antigos e atualiza com os novos que recebemos.

JobsDataBase: simplesmente a classe que cria o nosso banco de dados:

Aqui criamos o nosso database, referenciando nossa entidade criada anteriormente, e também setamos o nosso jobsDao como meio de acesso aos dados do banco. Temos o método createDataBase, que cria o nosso banco. Se quiser aprofundar mais sobre o Room, recomendo o seguinte artigo:

  • Mapper: aqui vamos mapear os dados para salvar na cache e também mapear os dados da cache para serem enviados corretamente.

Simplesmente uma classe object com funções para mapear os dados, podemos ver a utilização do mapper na seguinte interface JobsCacheSourceImpl na implementação do getJobs.

Quando recebemos o os jobs no getDao, eles estão vindo como entitdades do database, mas na verdaede o que precisamos retornar é uma lista de AndroidJobs, e pra isso chamamos no map o AndroidJobCacheMapper.map(DATA), passando os dados que o getDao retornou (nesse caso representado por it, que seria a lista que de jobs que esta no banco de dados) para serem traduzidos para List<AndroidJob> que é o dado que getJobs precisa retornar.

  • Source: é composto por uma interface que vai solicitar dados da cache e sua respectiva implementação:
  • JobsCacheDataSource: interface utilizada para que o repository possa solicitar dados da cache.

getJobs: retorna a lista de AndroidJobs mapeadas do banco de dados.

insertData: insere nossa lista de AndroidJobs no banco de dados.

updateData: atualiza nosso banco de dados.

  • JobsCacheSourceImpl: aqui temos a implementação da interface acima.

Dentro da cada implementação, o jobsDao executa a ação que estamos solicitando. Vale destacar que nas implementações de insert e update mapeamos os dados para salvar na nossa cache, e no getJobs mapeamos os dados da cache para os tipo requerido. O jobsDao é injetado no construtor.

RemoteData:

Agora que já temos a nossa cache, vamos nos preparar para receber os dados do backend.

  • Model: dado puro que vem do backend.

São as nossas entidades que representam o json que vem do backend, ou seja, o dado puro.

  • Api: simplesmente a interface que contém os endPoints, os quais vamos chamar para se comunicar com o backend.

Temos apenas uma chamada e nela estamos dizendo que esperamos o JobsPayload como retorno.

  • Mapper: aqui vamos mapear os dados puros do backend, nosso payloads, em AndroidJobs, que estão sendo pedidos pela domain.

Simplesmente uma classe object com funções para mapear os dados.

  • Source: é composto por uma interface que vai solicitar dados do backend e sua respectiva implementação:
  • RemoteDataSource: interface utilizada para que o repository possa solicitar dados do backend.

getJobs: retorna a lista de AndroidJobs, mapeadas do backend.

  • RemoteDataSourceImpl: implementação da interface acima.

Dentro da implementação do método getJobs, chamamos o endpoint da Api para solicitar os dados do backend, e, quando o recebemos, mapeamos o payload para o dado que foi solicitado. A serverApi é injetada no construtor.

RepositoryImpl:

  • AndroidJobsRepositoryImpl: enfim chegamos na implementação do AndroidJobsRepository da domain dentro do modulo data.

O único método que temos que implementar aqui é o getJobs, que deve retornar a lista de AndroidJobs.

Primeiro, verificamos se temos que forçar o update. Em caso positivo, chamamos o método getJobsRemote, que vai chamar o remoteDataSource, para pegar dados do backend, então salvá-los ou atualizá-los na cache por meio do jobsCacheDataSource. Caso não seja solicitado forçar o update, pegamos a lista da cache e, se ela for vazia, chamamos o getJobsRemote, salvamos na cache e retornamos à nossa lista.

Di(dependency injection):

Aqui ficam nossos modules Koin, que gerenciam nossas dependências.

  • DataCacheModule: responsável por gerir as dependências da cache.

Primeiro estamos criando a instância do nosso database, que é um single, ou seja é criado uma única vez durante o ciclo de vida do app.

Chamamos JobsDataBase.createDatabase para criar essa instância para nós.

Em seguida, criamos nosso provedor da cache, que é factory, ou seja, toda vez que for requerido, será criado uma nova instância, onde passamos a interface como provedor e, dentro da classe que o implementa, fornecemos no construtor a dependência requerida (nesse caso o jobsDao, que foi criada por meio do JobsDataBase.createDatabase).

  • DataRemoteModule: responsável por gerir as dependências do backend.

Como podemos ver, temos a criação do nosso webService e httpClient, que são coisas do retrofit. Então, recomendo este artigo caso não tenha experiência com Retrofit: https://medium.com/@prakash_pun/retrofit-a-simple-android-tutorial-48437e4e5a23

No método createWebService, primeiramente, provemos o nosso OkHttpClient, na criação do webService. Em seguida, passamos à interface ServerApi como um parâmetro reified, que nos possibilita utilizá-la dentro da função. Também passamos a nossa base url, que está nos resources. Esse método provê o nosso webService, que pode ser requerido por qualquer classe que tem a serverApi no seu construtor. Depois disso, temos um último factory, que possui a interface RemoteDataSource provedor e dentro RemoteDataSourceImpl, que recebe no construtor a dependência requerida (no caso, serverApi).

Duvidas de Koin: https://insert-koin.io/docs/2.0/getting-started/android/ Durante o desenvolvimento do artigo, mudamos a versão do koin para 2.0-rc1

  • DataModule: e finalmente, criamos a dependência do repository.

Agora que ja criamos as dependências necessárias para o nosso repositório, criamos o nosso module Koin recebendo-las, e como explicamos anteriormente o factory, será criada uma nova instancia toda vez que for requerido.

e por fim uma variável chamada dataModule, para juntar os Koin modules e um único lugar.


Finalmente chegamos ao fim de mais um capítulo dessa série. E, embora não tenha imaginado que esta parte ficaria tão grande, acredito que foi positivo passar por todas as etapas.

Data repo: https://github.com/ifucolo/android-modularization/tree/data

follow me: https://twitter.com/Iagolfucolo

Revisores:

Rafael Machado

Bruno Stone

Android Dev BR

Artigos em português sobre Android, curados pela comunidade Android Dev BR. Junte-se a nós: slack.androiddevbr.org.

Iago Mendes Fucolo

Written by

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

Android Dev BR

Artigos em português sobre Android, curados pela comunidade Android Dev BR. Junte-se a nós: slack.androiddevbr.org.