Implementando um cache para o Room com BehaviorSubject

Conhecimentos recomendados para melhor compreensão do post: Room, Reactive Programming e Kotlin.

Após adicionar o Room a um pet project eu decidi alterar os retornos para retornar Observables e remover todos os AsyncTasks do código.

Enquanto eu lia este artigo eu gostei bastante da forma como o Room funciona quando o retorno do Dao é um Flowable e na intenção de aprender mais sobre Reactive Programming, decidi implementar esse comportamento manualmente.

Para não simplesmente copiar o comportamento, decidi implementar também uma camada de cache. Com isso, os objetivos da implementação são:

  • Retornar um objeto onde os subscribers possam dar subscribe e ficar recebendo qualquer update que aconteça no modelo.
  • Implementar um cache de forma que quando um novo subscriber efetuar o subscribe, o valor atual do cache seja emitido para ele sem a necessidade de efetuar um acesso ao banco.

O primeiro passo foi encontrar a melhor forma de implementar isto com Reactive Programming e a minha escolha foi utilizar o BehaviorSubject.

Caso você saiba ler inglês, recomendo a leitura da documentação sobre subjects. Caso contrário, acredito que esta imagem simplifica muito bem o comportamento:

Fonte: http://reactivex.io/documentation/subject.html

Basicamente, cada novo subscriber recebe o último elemento e fica escutando o stream para receber novos updates.

Outro excelente exemplo é este código:

O output de execução é este:

Como você pode ver, a cada subscribe o último valor é emitido e a cada emissão todos os subscribers recebem o update.

Mantendo os créditos: copiei o exemplo desta pergunta no stackoverflow


Agora que temos o subject definido, podemos começar a implementação do Room.

Código do model:

Código do Dao:

Para abstrair o acesso ao banco, podemos implementar um DataSource, conforme a seguir:


Com todo o boilerplate criado, agora podemos finalmente começar a implementação do mecanismo de cache e subscribe.

Vamos dar o nome da classe que será responsável por implementar o mecanismo de DatabaseObservable:

Indo por partes:

  • O construtor espera dois lambdas, o generateData que é responsável por retornar os dados que estão no banco na primeira execução e o mergeData que irá ser chamado com o valor atual em cache e um novo dado e retornar os dados atualizados.
  • O método cache retorna o BehaviorSubject atual e o cria caso seja necessário. As chamadas para o método replay é para manter somente em cache o último elemento e o autoConnect é para emitir automaticamente a cada novo subscribe.
  • O método newData é chamado quando algum novo dado é salvo no banco, ele irá chamar o mergeData para atualizar o cache e emitir novamente o valor atualizado. O código bs!!.blockingFirst() é simplesmente para efetuar uma chamada síncrona para pegar o conteúdo atual que está no BehaviorSubject.

Com isso temos o mecanismo implementado, agora vamos utilizá-lo.

Novamente explicando o código:

  • O método generateData simplesmente retorna os dados atualmente na tabela
  • O método mergeData procura o elemento atual no array para efetuar a substituição, caso não o encontre, retornamos um novo array com o elemento adicionado ao final
  • O método all simplesmente retorna um Observable com o valor atual em cache
  • O método save faz o mesmo trabalho de salvar, somente agora temos o trabalho de notificar o DatabaseObservable que houve uma alteração nos dados.

Em resumo, é uma solução bem complexa para um problema simples, eu só a utilizaria em algum projeto real caso precise ter algum controle especial sobre o cache, como por exemplo invalidá-lo após alterações em outros tabelas. Caso você simplesmente necessite de um observable para a tabela, o Flowable do Room é uma solução mais do que suficiente.

Todo o código provido neste post está disponível neste repositório.

Agradecimentos a Thainara Rogério pela revisão.