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:
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.