Padrão do repositório

1. Antes de começar

Introdução

Neste codelab, você vai melhorar a experiência do usuário de um app usando o armazenamento em cache off-line. Muitos apps dependem de dados da rede. Se o app busca dados do servidor em todas as inicializações e o usuário vê uma tela de carregamento, a experiência dele pode ser ruim. Isso pode fazer com que os usuários desinstalem o app.

Quando os usuários iniciam um app, eles esperam que os dados sejam mostrados rapidamente. Para alcançar esse objetivo, implemente o armazenamento em cache off-line. Com o armazenamento em cache off-line, o app salva dados buscados da rede no armazenamento local do dispositivo, resultando em um acesso mais rápido.

Como o app pode receber dados da rede, além de manter um cache off-line dos resultados previamente transferidos, você vai precisar organizar o conteúdo a fim de usar essas várias fontes. Para fazer isso, implemente uma classe de repositório, que vai servir como uma única fonte de verdade dos dados do app, e abstraia a fonte dos dados (rede, cache etc.) do modelo de visualização.

O que você já precisa saber

Você precisa:

O que você vai aprender

  • Como implementar um repositório para abstrair a camada de dados de um app do restante dele.
  • Como carregar dados armazenados em cache usando um repositório.

O que você vai fazer

  • Usar um repositório para abstrair a camada de dados e integrar a classe de repositório ao ViewModel.
  • Mostrar dados do cache off-line.

2. Código inicial

Fazer o download do código do projeto

O nome da pasta é RepositoryPattern-Starter. Selecione essa pasta ao abrir o projeto no Android Studio.

Para encontrar o código deste codelab e abrir no Android Studio, faça o seguinte.

Acessar o código

  1. Clique no URL fornecido. Isso vai abrir a página do GitHub referente ao projeto em um navegador.
  2. Verifique e confirme se o nome da ramificação mostrado corresponde ao nome da ramificação especificado no codelab. Por exemplo, na captura de tela a seguir, o nome da ramificação é main.

8cf29fa81a862adb.png

  1. Na página do GitHub do projeto, clique no botão Code, que vai mostrar uma janela pop-up.

1debcf330fd04c7b.png

  1. Na janela pop-up, clique no botão Download ZIP para salvar o projeto no seu computador. Aguarde a conclusão do download.
  2. Localize o arquivo no computador. Geralmente ele é salvo na pasta Downloads.
  3. Clique duas vezes para descompactar o arquivo ZIP. Isso cria uma nova pasta com os arquivos do projeto.

Abrir o projeto no Android Studio

  1. Inicie o Android Studio.
  2. Na janela Welcome to Android Studio, clique em Open.

d8e9dbdeafe9038a.png

Observação: caso o Android Studio já esteja aberto, selecione a opção File > Open.

8d1fda7396afe8e5.png

  1. No navegador de arquivos, vá até a pasta descompactada do projeto, que provavelmente está na pasta Downloads.
  2. Clique duas vezes nessa pasta do projeto.
  3. Aguarde o Android Studio abrir o projeto.
  4. Clique no botão Run 8de56cba7583251f.png para criar e executar o app. Confira se ele é criado da forma esperada.

3. Visão geral do app inicial

O app DevBytes apresenta uma lista de vídeos do Canal do YouTube Android Developers em uma visualização de reciclagem, em que os usuários podem clicar para abrir um link do vídeo.

9757e53b89d2de7c.png

Embora o código inicial esteja funcionando totalmente, ele tem uma grande falha que pode afetar negativamente a experiência do usuário. Se o usuário tiver uma conexão instável ou não tiver conexão com a Internet, nenhum dos vídeos vai ser exibido. Isso acontece mesmo se o app tiver sido aberto anteriormente. Se o usuário sair e reiniciar o DevBytes, mas sem Internet, o app tenta fazer o download da lista de vídeos novamente, mas não consegue.

Você pode ver isso em ação no emulador.

  1. Ative temporariamente o modo avião no Android Emulator emConfigurações do app > Rede e Internet > Modo avião.
  2. Execute o app DevBytes e observe que a tela está em branco.

f0365b27d0dd8f78.png

  1. Desative o modo avião antes de continuar com o restante do codelab.

Isso ocorre porque, depois que o app DevBytes faz o download dos dados pela primeira vez, nada é armazenado em cache para uso futuro. No momento, o app inclui um banco de dados do Room. Sua tarefa é usar esse banco de dados para implementar a funcionalidade de armazenamento em cache e atualizar o modelo de visualização a fim de usar um repositório que vai fazer o download de novos dados ou os buscar no banco de dados do Room. A classe de repositório abstrai essa lógica do modelo de visualização, mantendo seu código organizado e desacoplado.

O projeto inicial é organizado em vários pacotes.

25b5f8d0997df54c.png

Embora possa se familiarizar com o código, você só precisa trabalhar com dois arquivos: repository/VideosRepository.kt e viewmodels/DevByteViewModel. Primeiro, você vai criar uma classe VideosRepository que implementa o padrão do repositório para armazenamento em cache. Você vai saber mais sobre isso nas próximas páginas. Depois, vai atualizar o DevByteViewModel para usar a nova classe VideosRepository.

Antes de ir direto ao código, reserve um momento para saber mais sobre o armazenamento em cache e o padrão do repositório.

4. Armazenamento em cache e o padrão do repositório

Repositórios

O padrão do repositório é um padrão de design que isola a camada de dados do restante do app. A camada de dados se refere à parte do app, separada da IU, que gerencia os dados e a lógica de negócios dele, expondo APIs consistentes para que o restante do app acesse esses dados. Enquanto a IU apresenta informações ao usuário, a camada de dados inclui itens como código de rede, bancos de dados do Room, tratamento de erros e qualquer código que leia ou manipule dados.

9e528301efd49aea.png

Um repositório pode resolver conflitos entre fontes de dados, como modelos persistentes, serviços da Web e caches, além de centralizar as mudanças nesses dados. O diagrama abaixo mostra como componentes de apps, como atividades, podem interagir com fontes de dados usando um repositório.

69021c8142d29198.png

Para implementar um repositório, você usa uma classe separada, como a classe VideosRepository criada na próxima tarefa. A classe de repositório isola as fontes de dados do restante do app e fornece a ele uma API limpa para o acesso aos dados. Usar uma classe de repositório garante que esse código seja separado da classe ViewModel e é uma prática recomendada para a separação e arquitetura do código.

Vantagens de usar um repositório

Um módulo de repositório lida com operações de dados e permite usar vários back-ends. Em um app real típico, o repositório implementa a lógica para decidir se precisa buscar dados de uma rede ou usar resultados armazenados em cache em um banco de dados local. Com um repositório, é possível trocar os detalhes de implementação, migrando para uma biblioteca de persistência diferente, por exemplo, sem afetar o código de chamada, como os modelos de visualização. Isso também ajuda a tornar o código modular e testável. É fácil simular o repositório e testar o restante do código.

Um repositório serve como uma única fonte confiável para determinada parte dos dados do seu app. Ao trabalhar com várias fontes de dados, como um recurso em rede e um cache off-line, o repositório garante que os dados sejam precisos e atualizados. Isso proporciona a melhor experiência possível, mesmo quando o app está off-line.

Armazenamento em cache

Um cache (link em inglês) se refere a um armazenamento de dados usados pelo app. Por exemplo, você pode querer salvar temporariamente os dados da rede caso a conexão de Internet do usuário seja interrompida. Mesmo que a rede não esteja mais disponível, o app ainda pode recorrer aos dados armazenados em cache. Um cache também pode ser útil para armazenar dados temporários de uma atividade que não está mais na tela ou até mesmo para persistir dados entre inicializações de apps.

Um cache pode assumir várias formas, mais simples ou mais complexas, dependendo da tarefa específica. A tabela abaixo mostra várias maneiras de implementar o armazenamento em cache de rede no Android.

Técnica de armazenamento em cache

Usos

A Retrofit (link em inglês) é uma biblioteca de rede usada para implementar um cliente REST de tipo seguro para o Android. Você pode configurar a Retrofit para armazenar localmente uma cópia de cada resultado de rede.

Essa é uma boa solução para solicitações e respostas simples, chamadas de rede pouco frequentes ou conjuntos de dados pequenos.

Use o DataStore para armazenar pares de chave-valor.

Uma boa solução para um pequeno número de chaves e valores simples, como configurações de apps. Não é possível usar essa técnica para armazenar grandes quantidades de dados estruturados.

Você pode acessar o diretório de armazenamento interno do app e salvar arquivos de dados nele. O nome do pacote do app especifica o diretório de armazenamento interno, que está em um local especial no sistema de arquivos do Android. Esse diretório é privado para o app, e é excluído quando o app é desinstalado.

Essa é uma boa solução se você tiver necessidades específicas que um sistema de arquivos pode resolver. Por exemplo, se precisar salvar arquivos de mídia ou de dados e precisar os gerenciar por conta própria. Não é possível usar essa técnica para armazenar dados complexos e estruturados que o app precisa consultar.

Você pode armazenar dados em cache usando o Room, uma biblioteca de mapeamento de objetos SQLite que fornece uma camada de abstração sobre o SQLite.

Solução recomendada para dados estruturados complexos consultáveis, porque a melhor maneira de armazenar dados estruturados no sistema de arquivos de um dispositivo é em um banco de dados SQLite local.

Neste codelab, você usa o Room porque essa é a maneira recomendada de armazenar dados estruturados em um sistema de arquivos do dispositivo. O app DevBytes já está configurado para usar o Room. Sua tarefa é implementar o armazenamento em cache off-line usando o padrão do repositório para separar a camada de dados do código da IU.

5. Implementar o VideosRepository

Tarefa: criar um repositório

Nesta tarefa, você vai criar um repositório para gerenciar o cache off-line que foi implementado na tarefa anterior. O banco de dados do Room não tem lógica para gerenciar o cache off-line, apenas métodos para inserir, atualizar, excluir e extrair dados. O repositório vai ter a lógica para buscar os resultados da rede e manter o banco de dados atualizado.

Etapa 1: adicionar um repositório

  1. Em repository/VideosRepository.kt, crie uma classe VideosRepository. Transmita um objeto VideosDatabase como o parâmetro do construtor da classe para acessar os métodos DAO.
class VideosRepository(private val database: VideosDatabase) {
}
  1. Na classe VideosRepository, adicione um método suspend com o nome refreshVideos() que não tem argumentos e não retorna nada. Esse método vai ser a API usada para atualizar o cache off-line.
suspend fun refreshVideos() {
}
  1. Dentro do método refreshVideos(), mude o contexto da corrotina para Dispatchers.IO a fim de fazer operações de rede e de banco de dados.
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
   }
}
  1. No bloco withContext, busque a playlist de vídeos DevByte na rede usando a instância de serviço da Retrofit, DevByteNetwork.
val playlist = DevByteNetwork.devbytes.getPlaylist()
  1. No método refreshVideos(), após buscar a playlist na rede, armazene ela no banco de dados do Room. Para armazenar a playlist, use a classe VideosDatabase. Chame o método DAO insertAll() transmitindo a playlist extraída da rede. Use a função de extensão asDatabaseModel() a fim de mapear a playlist para o objeto do banco de dados.
database.videoDao.insertAll(playlist.asDatabaseModel())
  1. Este é o método refreshVideos() completo com um log statement que monitora quando ele é chamado:
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
       val playlist = DevByteNetwork.devbytes.getPlaylist()
       database.videoDao.insertAll(playlist.asDatabaseModel())
   }
}

Etapa 2: extrair dados do banco de dados

Nesta etapa, você cria um objeto LiveData para ler a playlist de vídeos do banco de dados. Esse objeto LiveData é atualizado automaticamente junto do banco de dados. O fragmento ou a atividade anexado é atualizado com novos valores.

  1. Na classe VideosRepository, declare um objeto LiveData com o nome videos para armazenar uma lista de objetos DevByteVideo. Inicialize o objeto videos usando o database.videoDao. Chame o método DAO getVideos(). Como o método getVideos() retorna uma lista de objetos do banco de dados, e não uma lista de objetos DevByteVideo, o Android Studio gera um erro de "incompatibilidade de tipos".
val videos: LiveData<List<DevByteVideo>> = database.videoDao.getVideos()
  1. A fim de corrigir esse erro, use Transformations.map para converter a lista de objetos do banco de dados em uma lista de objetos do domínio usando a função de conversão asDomainModel().
val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) {
   it.asDomainModel()
}

Você implementou um repositório para o app. Na próxima tarefa, você vai usar uma estratégia simples de atualização a fim de manter o banco de dados local atualizado.

6. Usar o VideosRepository no DevByteViewModel

Tarefa: integrar o repositório usando uma estratégia de atualização

Nesta tarefa, você vai integrar seu repositório ao ViewModel usando uma estratégia simples de atualização. Você mostra a playlist de vídeos no banco de dados do Room sem precisar buscar diretamente na rede.

A atualização de um banco de dados é um processo para que o banco de dados esteja sincronizado com os dados da rede. Neste app de exemplo, você vai usar uma estratégia de atualização simples em que o módulo que solicita dados do repositório é responsável por atualizar os dados locais.

Em um app real, a estratégia pode ser mais complexa. Por exemplo, o código pode atualizar automaticamente os dados em segundo plano, considerando a largura de banda, ou armazenar em cache os dados que o usuário provavelmente vai usar em seguida.

  1. Em viewmodels/DevByteViewModel.kt, dentro da classe DevByteViewModel, crie uma variável de membro privado com o nome videosRepository do tipo VideosRepository. Instancie a variável transmitindo o objeto VideosDatabase Singleton.
private val videosRepository = VideosRepository(getDatabase(application))
  1. Na classe DevByteViewModel, substitua o método refreshDataFromNetwork() pelo refreshDataFromRepository(). O método antigo, refreshDataFromNetwork(), buscava a playlist de vídeos da rede usando a biblioteca Retrofit. O novo método carrega a playlist de vídeos usando o repositório. O repositório determina de qual origem (rede ou banco de dados) a playlist é extraída, mantendo os detalhes de implementação fora do modelo de visualização. O repositório também torna o código mais sustentável. Se você mudar a implementação para acessar os dados no futuro, não vai ser preciso modificar o modelo de visualização.
private fun refreshDataFromRepository() {
   viewModelScope.launch {
       try {
           videosRepository.refreshVideos()
           _eventNetworkError.value = false
           _isNetworkErrorShown.value = false

       } catch (networkError: IOException) {
           // Show a Toast error message and hide the progress bar.
           if(playlist.value.isNullOrEmpty())
               _eventNetworkError.value = true
       }
   }
}
  1. Na classe DevByteViewModel, no bloco init, mude a chamada de função de refreshDataFromNetwork() para refreshDataFromRepository(). Esse código busca a playlist de vídeos no repositório, não diretamente na rede.
init {
   refreshDataFromRepository()
}
  1. Na classe DevByteViewModel, exclua a propriedade _playlist e a propriedade de apoio, playlist.

Código a ser excluído

private val _playlist = MutableLiveData<List<Video>>()
...
val playlist: LiveData<List<Video>>
   get() = _playlist
  1. Na classe DevByteViewModel, depois de instanciar o objeto videosRepository, adicione uma nova variável val com o nome playlist para armazenar uma lista de vídeos LiveData do repositório.
val playlist = videosRepository.videos
  1. Execute o app. Ele vai ser executado como antes, mas agora a playlist DevBytes é buscada na rede e salva no banco de dados do Room. A playlist é mostrada na tela pelo banco de dados do Room, não diretamente pela rede.

30ee74d946a2f6ca.png

  1. Para perceber a diferença, ative o modo avião no emulador ou dispositivo.
  2. Execute o app novamente. Observe que a mensagem de aviso "Erro de rede" não é mostrada. Em vez disso, a playlist é buscada do cache off-line e exibida.
  3. Desative o modo avião no emulador ou dispositivo.
  4. Feche e abra novamente o app. Ele carrega a playlist do cache off-line, enquanto a solicitação de rede é executada em segundo plano.

Se novos dados forem extraídos da rede, a tela vai ser atualizada automaticamente para os mostrar. No entanto, o servidor do DevBytes não atualiza o conteúdo. Por isso, você não vê os dados sendo atualizados.

Bom trabalho! Neste codelab, você integrou um cache off-line a um ViewModel para mostrar a playlist do repositório em vez de a buscar na rede.

7. Código da solução

Código da solução

Projeto do Android Studio: RepositoryPattern (link em inglês)

8. Parabéns

Parabéns! Nesse programa, você aprendeu que:

  • O Armazenamento em cache (link em inglês) é o processo de armazenar dados buscados de uma rede no armazenamento de um dispositivo. Ele permite que o app acesse dados quando o dispositivo estiver off-line ou se precisar acessar os mesmos dados novamente.
  • A melhor maneira do app armazenar dados estruturados no sistema de arquivos de um dispositivo é usar um banco de dados SQLite local. O Room é uma biblioteca de mapeamento de objetos SQLite, o que significa que fornece uma camada de abstração sobre o SQLite. Usar o Room é a prática recomendada para implementar o armazenamento em cache off-line.
  • Uma classe de repositório isola as fontes de dados, como serviços da Web e um banco de dados do Room, das outras partes do app. Essa classe fornece uma API limpa para acesso aos dados no restante do app.
  • Usar repositórios é uma prática recomendada para a separação e arquitetura do código.
  • Ao projetar um cache off-line, é recomendável separar os objetos de rede, domínio e banco de dados do app. Essa estratégia é um exemplo de separação de conceitos (link em inglês).

Saiba mais