Introdução

Neste tutorial, você melhora a experiência do usuário para um aplicativo usando o cache offline. Muitos aplicativos dependem de dados da rede. Se o seu aplicativo busca dados do servidor em cada inicialização, o usuário pode ver uma tela de carregamento e isso pode ser uma experiência ruim para o usuário. Os usuários podem desinstalar seu aplicativo.

Quando os usuários iniciam um aplicativo, eles esperam que o aplicativo mostre dados rapidamente. Você pode atingir esse objetivo implementando o cache offline. Cache offline significa que seu aplicativo salva dados buscados da rede no armazenamento local do dispositivo, para acesso mais rápido.

Muitos usuários possuem acesso intermitente à Internet. Ao implementar o cache offline, você adiciona suporte offline ao seu aplicativo, ajudando esses usuários a usar o seu aplicativo enquanto estão offline.

O que você já deveria saber

Você deve estar familiarizado com:

O que aprenderá

O que fará

O aplicativo DevBytes exibe uma lista de vídeos DevByte, que são tutoriais curtos feitos pela equipe de relações de desenvolvedor do Google Android. Os vídeos apresentam os recursos do desenvolvedor e as práticas recomendadas para o desenvolvimento Android.

O aplicativo inicial DevBytes busca uma lista de URLs de vídeo da rede usando a biblioteca Retrofit e exibe a lista usando um RecyclerView. O aplicativo usa ViewModel e LiveData para manter os dados e atualizar a IU. A arquitetura do aplicativo é semelhante aos aplicativos que você desenvolveu anteriormente neste curso.

O aplicativo inicial é apenas online, então o usuário precisa de uma conexão de rede para usá-lo. Neste tutorial, você implementa o cache offline para exibir os resultados do banco de dados local, em vez da rede. Seus usuários poderão usar o aplicativo enquanto seus dispositivos estiverem offline ou se eles tiverem uma conexão de rede lenta.

Para implementar o cache offline, você usa um banco de dados Room para tornar os dados buscados persistentes no armazenamento local do dispositivo. Você acessa e gerencia o banco de dados Room usando um padrão de repositório, que é um padrão de projeto que isola as fontes de dados do resto do aplicativo. Essa técnica fornece uma API limpa para o restante do aplicativo usar para acessar os dados.

Nesta tarefa, você baixa e inspeciona o código inicial para o aplicativo DevBytes.

Etapa 1: Baixe e execute o aplicativo inicial

  1. Baixe o código DevBytes inicial do GitHub.
  2. Descompacte o código e abra o projeto no Android Studio.
  3. Conecte seu dispositivo de teste ou emulador à Internet, se ainda não estiver conectado. Construa e execute o aplicativo. O aplicativo busca uma lista de vídeos DevByte da rede e os exibe.
  4. No aplicativo, clique em qualquer vídeo para abri-lo no aplicativo do YouTube.
  5. Ative o modo avião no seu dispositivo ou emulador.
  6. Execute o aplicativo novamente e observe a mensagem do sistema de erro de rede.

Quando o modo avião está desligado, você pode ver uma barra de progresso girando se sua conexão com a Internet estiver lenta, pois este é um aplicativo apenas online. Se você não vir a barra de progresso girando, implemente a próxima etapa para adicionar atraso de rede programaticamente. Isso ajudará você a ver como é a experiência do aplicativo para usuários com conexões lentas e por que o cache offline é importante para este aplicativo.

Etapa 2: (opcional) simular atraso de rede

Se a conexão com a Internet para seu emulador ou dispositivo estiver boa e você não notar a barra de progresso girando, simule o atraso na resposta da rede usando a função delay(). Para aprender mais sobre a função delay(), veja Sua primeira corrotina com Kotlin.

  1. Certifique-se de que o modo avião esteja desativado em seu dispositivo ou emulador.
  2. Em DevByteViewModel, dentro de refreshDataFromNetwork(), no início do bloco catch, adicione um atraso de 2 segundos. Esse atraso suspenderá a corrotina que busca dados da rede.
private fun refreshDataFromNetwork() = viewModelScope.launch {

   try {
        ...
   } catch (networkError: IOException) {
       delay(2000)
       _eventNetworkError.value = true
   }
}
  1. Execute o aplicativo novamente. Agora você vê um botão giratório de carregamento e a mensagem do sistema de erro de rede. O botão giratório de carregamento é o que seus usuários podem ver se tiverem conexões de rede lentas. Depois de implementar o cache offline, a experiência do usuário será aprimorada.

  1. Remova a instrução de atraso, delay(2000), que você adicionou em uma etapa anterior.

Etapa 3: Explore o código

Este aplicativo inicial vem com muito código, em particular todos os módulos de rede e interface do usuário, para que você possa se concentrar no módulo de repositório do aplicativo.

  1. No Android Studio, expanda todos os pacotes.
  2. Explore o pacote domain. Este pacote contém classes de data para representar os dados do aplicativo. Por exemplo, a classe de dados DevByteVideo na classe domain/Models.kt representa um único vídeo DevByte.
  3. Explore o pacote network.

    A classe network/DataTransferObjects.kt contém a classe de dados para um objeto de transferência de dados chamado NetworkVideo. O objeto de transferência de dados é usado para analisar o resultado da rede. Este arquivo também contém um método conveniente, asDomainModel(), para converter os resultados da rede em uma lista de objetos de domínio. Os objetos de transferência de dados são diferentes dos objetos de domínio, pois contêm lógica extra para analisar os resultados da rede.
  1. Experimente explorar o resto do código inicial por conta própria.

O resto da arquitetura dos aplicativos é semelhante aos outros aplicativos usados ​​nos tutoriais anteriores:

Depois que um aplicativo busca dados da rede, o aplicativo pode armazenar em cache os dados, armazenando-os no armazenamento de um dispositivo. Você armazena dados em cache para que possa acessá-los mais tarde, quando o dispositivo estiver off-line ou se desejar acessar os mesmos dados novamente.

A tabela a seguir mostra várias maneiras de implementar o cache de rede no Android. Neste tutorial, você usa Room, pois é a maneira recomendada de armazenar dados estruturados em um sistema de arquivos de dispositivo.

Técnica de cache

Usos

Retrofit é uma biblioteca de rede usada para implementar um cliente REST de tipo seguro para Android. Você pode configurar o Retrofit para armazenar uma cópia de cada resultado da rede localmente.

Boa solução para solicitações e respostas simples, chamadas de rede não frequentes ou pequenos conjuntos de dados.

Você pode usar SharedPreferences para armazenar pares de valor-chave.

Boa solução para um pequeno número de chaves e valores simples. Você não pode usar essa técnica para armazenar grandes quantidades de dados estruturados.

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

Boa solução se você tem necessidades específicas que um sistema de arquivos pode resolver - por exemplo, se você precisa salvar arquivos de mídia ou arquivos de dados e tem que gerenciar os arquivos você mesmo. Você não pode usar essa técnica para armazenar dados complexos e estruturados.

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

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

Nesta tarefa, você adiciona um banco de dados Room ao seu aplicativo para usar como um cache offline.

Quando o aplicativo busca dados da rede, armazene os dados no banco de dados em vez de exibi-los imediatamente.

Quando um novo resultado de rede for recebido, atualize o banco de dados local e exiba o novo conteúdo na tela do banco de dados local. Essa técnica garante que o cache offline esteja sempre atualizado. Além disso, se o dispositivo estiver offline, seu aplicativo ainda pode carregar dados armazenados em cache localmente.

Etapa 1: Adicione a dependência de Room

  1. Abra o arquivo build.gradle (Module:app) e adicione a dependência Room ao projeto.
def room_version = "2.1.0-alpha06"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

Etapa 2: Adicione objeto de banco de dados

Nesta etapa, você cria uma entidade de banco de dados chamada DatabaseVideo para representar objetos de banco de dados. Você também implementa métodos de conveniência para converter objetos DatabaseVideo em objetos de domínio e para converter objetos de rede em objetos DatabaseVideo.

  1. Abra database/DatabaseEntities.kt e crie uma entidade Room chamada DatabaseVideo. Defina url como a chave primária. O design do servidor DevBytes garante que a URL do vídeo seja sempre exclusiva.
@Entity
data class DatabaseVideo constructor(
       @PrimaryKey
       val url: String,
       val updated: String,
       val title: String,
       val description: String,
       val thumbnail: String)
  1. Em database/DatabaseEntities.kt, crie uma função de extensão chamada asDomainModel(). Use a função para converter objetos de banco de dados DatabaseVideo em objetos de domínio.
fun List<DatabaseVideo>.asDomainModel(): List<DevByteVideo> {
   return map {
       DevByteVideo(
               url = it.url,
               title = it.title,
               description = it.description,
               updated = it.updated,
               thumbnail = it.thumbnail)
   }
}

Neste aplicativo de amostra, a conversão é simples e parte desse código não é necessária. Mas em um aplicativo do mundo real, a estrutura dos objetos de domínio, banco de dados e rede será diferente. Você precisará de lógica de conversão, o que pode ser complicado.

  1. Abra network/DataTransferObjects.kt e crie uma função de extensão chamada asDatabaseModel(). Use a função para converter objetos de rede em objetos de banco de dados DatabaseVideo.

fun NetworkVideoContainer.asDatabaseModel(): List<DatabaseVideo> {
   return videos.map {
       DatabaseVideo(
               title = it.title,
               description = it.description,
               url = it.url,
               updated = it.updated,
               thumbnail = it.thumbnail)
   }
}

Etapa 3: Adicione VideoDao

Nesta etapa, você implementa VideoDao e define dois métodos auxiliares para acessar o banco de dados. Um método auxiliar obtém vídeos do banco de dados e o outro método insere vídeos no banco de dados.

  1. Em database/Room.kt, defina uma interface VideoDao e anote com @Dao.
@Dao
interface VideoDao { 
}
  1. Dentro da interface VideoDao, crie um método chamado getVideos() para buscar todos os vídeos do banco de dados. Altere o tipo de retorno deste método para LiveData, de modo que os dados exibidos na IU sejam atualizados sempre que os dados no banco de dados forem alterados.
   @Query("select * from databasevideo")
   fun getVideos(): LiveData<List<DatabaseVideo>>

Se um erro de Unresolved reference aparecer no Android Studio, importe androidx.room.Query.

  1. Dentro da interface VideoDao, defina outro método insertAll() para inserir uma lista de vídeos obtidos da rede no banco de dados. Para simplificar, substitua a entrada do banco de dados se a entrada de vídeo já estiver presente no banco de dados. Para fazer isso, use o argumento onConflict para definir a estratégia de conflito para REPLACE.
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll( videos: List<DatabaseVideo>)

Etapa 4: Implemente RoomDatabase

Nesta etapa, você adiciona o banco de dados para seu cache offline implementando RoomDatabase.

  1. Em database/Room.kt, após a interface VideoDao, crie uma classe abstract chamada VideosDatabase. Estenda VideosDatabase de RoomDatabase.
  2. Use a anotação @Database para marcar a classe VideosDatabase como um banco de dados Room. Declare a entidade DatabaseVideo que pertence a este banco de dados e defina o número da versão para 1.
  3. Dentro de VideosDatabase, defina uma variável do tipo VideoDao para acessar os métodos Dao.
@Database(entities = [DatabaseVideo::class], version = 1)
abstract class VideosDatabase: RoomDatabase() {
   abstract val videoDao: VideoDao
}
  1. Crie uma variável private lateinit chamada INSTANCE fora das classes, para manter o singleton objeto. O VideosDatabase deve ser singleton para evitar que várias instâncias do banco de dados sejam abertas ao mesmo tempo.
  2. Crie e defina um método getDatabase() fora das classes. Em getDatabase(), inicialize e retorne a variável INSTANCE dentro do bloco synchronized.
@Dao
interface VideoDao {
...
}
abstract class VideosDatabase: RoomDatabase() {
...
}

private lateinit var INSTANCE: VideosDatabase

fun getDatabase(context: Context): VideosDatabase {
   synchronized(VideosDatabase::class.java) {
       if (!::INSTANCE.isInitialized) {
           INSTANCE = Room.databaseBuilder(context.applicationContext,
                   VideosDatabase::class.java,
                   "videos").build()
       }
   }
   return INSTANCE
}

Agora você implementou o banco de dados usando Room. Na próxima tarefa, você aprenderá como usar esse banco de dados usando um padrão de repositório.

O padrão de repositório

O padrão de repositório é um padrão de projeto que isola fontes de dados do resto do aplicativo.

Um repositório faz a mediação entre as fontes de dados (como modelos persistentes, serviços da web e caches) e o resto do aplicativo. O diagrama abaixo mostra como os componentes do aplicativo, como atividades que usam LiveData, podem interagir com fontes de dados por meio de um repositório.

Para implementar um repositório, você usa uma classe de repositório, como a classe VideosRepository que você criará na próxima tarefa. A classe de repositório isola as fontes de dados do restante do aplicativo e fornece uma API limpa para acesso aos dados para o restante do aplicativo. Usando uma classe de repositório é uma prática recomendada para separação e arquitetura de código.

Vantagens de usar um repositório

Um módulo de repositório trata com operações de dados e permite que você use vários backends. Em um aplicativo típico do mundo real, o repositório implementa a lógica para decidir se deve buscar dados de uma rede ou usar resultados que são armazenados em cache em um banco de dados local. Isso ajuda a tornar seu código modular e testável. Você pode simular facilmente o repositório e testar o resto do código.

Nesta tarefa, você cria um repositório para gerenciar o cache offline, que você implementou na tarefa anterior. Seu banco de dados Room não tem lógica para gerenciar o cache offline, ele somente tem métodos para inserir e recuperar os dados. O repositório terá a lógica de buscar os resultados da rede e manter o banco de dados atualizado.

Etapa 1: Adicione um repositório

  1. Em repository/VideosRepository.kt, crie uma classe VideosRepository. Passe um objeto VideosDatabase como o parâmetro do construtor da classe para acessar os métodos Dao.
class VideosRepository(private val database: VideosDatabase) {
}
  1. Dentro da classe VideosRepository, adicione um método refreshVideos() que não tenha argumentos e não retorne nada. Este método será a API usada para atualizar o cache offline.
  2. Faça de refreshVideos() uma função de suspensão. Como refreshVideos() executa uma operação de banco de dados, ele deve ser chamado a partir de uma corrotina.
  1. Dentro do método refreshVideos(), mude o contexto da corrotina para Dispatchers.IO para executar operações de rede e banco de dados.

suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
   }
}
  1. Dentro do bloco withContext, busque a lista de reprodução de vídeo DevByte da rede usando a instância do serviço Retrofit, DevByteNetwork. Use a função await() para suspender a corrotina até que a lista de reprodução esteja disponível.
val playlist = DevByteNetwork.devbytes.getPlaylist().await()       
  1. Dentro do método refreshVideos(), depois de buscar a lista de reprodução na rede, armazene a lista de reprodução no banco de dados Room.

    Para armazenar a lista de reprodução, use o objeto VideosDatabase, database. Chame o método DAO insertAll, passando a playlist recuperada da rede. Use a função de extensão asDatabaseModel() para mapear a playlist para o objeto de banco de dados.
database.videoDao.insertAll(playlist.asDatabaseModel())
  1. Aqui está o método refreshVideos completo com uma instrução de registro para rastrear quando é chamado:
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
       Timber.d("refresh videos is called");
       val playlist = DevByteNetwork.devbytes.getPlaylist().await()
       database.videoDao.insertAll(playlist.asDatabaseModel())
   }
}

Etapa 2: Recupere dados do banco de dados

Nesta etapa, você cria um objeto LiveData para ler a lista de reprodução de vídeo do banco de dados. Este objeto LiveData é atualizado automaticamente quando o banco de dados é atualizado. O fragmento anexado, ou a atividade, é atualizado com novos valores.

  1. Na classe VideosRepository, declare um objeto LiveData chamado videos para conter uma lista de DevByteVideo objetos.
  2. Inicialize o objeto videos, usando database.videoDao. Chame o método DAO getVideos(). Como o método getVideos() retorna uma lista de objetos de banco de dados, não uma lista de objetos DevByteVideo, o Android Studio emite um erro de "incompatibilidade de tipo".
val videos: LiveData<List<DevByteVideo>> = database.videoDao.getVideos()
  1. Para corrigir o erro, use Transformations.map para converter a lista de objetos de banco de dados em uma lista de objetos de domínio. Use a função de conversão asDomainModel().
val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) {
   it.asDomainModel()
}

Agora você implementou um repositório para seu aplicativo. Na próxima tarefa, você usará uma estratégia de atualização simples para manter o banco de dados local atualizado.

Nesta tarefa, você integra seu repositório com o ViewModel usando uma estratégia de atualização simples. Você exibe a lista de reprodução de vídeo do banco de dados Room, não obtendo diretamente da rede.

Uma atualização do banco de dados é um processo de atualização ou atualização do banco de dados local para mantê-lo sincronizado com os dados da rede. Para este aplicativo de amostra, você usa uma estratégia de atualização muito simples, em que o módulo que solicita dados do repositório é responsável por atualizar os dados locais.

Em um aplicativo do mundo real, sua estratégia pode ser mais complexa. Por exemplo, seu código pode atualizar automaticamente os dados em segundo plano (levando em consideração a largura de banda) ou armazenar em cache os dados que o usuário provavelmente usará em seguida.

  1. Em viewmodels/DevByteViewModel.kt, dentro da classe DevByteViewModel, crie uma variável de membro private chamada videosRepository do tipo VideosRepository. Instancie a variável passando o objeto singleton VideosDatabase.
private val videosRepository = VideosRepository(getDatabase(application))
  1. Na classe DevByteViewModel, substitua o método refreshDataFromNetwork() pelo método refreshDataFromRepository().

O método antigo, refreshDataFromNetwork(), buscava a lista de reprodução de vídeo da rede usando a biblioteca Retrofit. O novo método carrega a lista de reprodução de vídeo do repositório.

private fun refreshDataFromRepository() {
   viewModelScope.launch {
       try {
           videosRepository.refreshVideos()
           _eventNetworkError.value = false
           _isNetworkErrorShown.value = false

       } catch (networkError: IOException) {
           if(playlist.value.isNullOrEmpty())
               _eventNetworkError.value = true
       }
   }
}
  1. Na classe DevByteViewModel, dentro do bloco init, altere a chamada de função de refreshDataFromNetwork() para refreshDataFromRepository(). Este código busca a lista de reprodução de vídeo do repositório, não diretamente da rede.
init {
   refreshDataFromRepository()
}
  1. Na classe DevByteViewModel, exclua a propriedade _playlist e sua propriedade de apoio, playlist.

Código para excluir:

private val _playlist = MutableLiveData<List<Video>>()
...
val playlist: LiveData<List<Video>>
   get() = _playlist
  1. Na classe DevByteViewModel, após instanciar o objeto videosRepository, adicione um novo objeto val chamado playlist para conter uma lista LiveData de vídeos do repositório.
val playlist = videosRepository.videos
  1. Execute seu aplicativo. O aplicativo funciona como antes, mas agora a lista de reprodução DevBytes é buscada na rede e salva no banco de dados Room. A lista de reprodução é exibida na tela do banco de dados Room, não diretamente da rede.

  1. Para notar a diferença, habilite o modo avião no emulador ou dispositivo.
  2. Execute o aplicativo novamente. Observe que a mensagem de notificação "Erro de rede" não é exibida; em vez disso, a lista de reprodução é obtida do cache offline e exibida.
  3. Desative o modo avião no emulador ou dispositivo.
  4. Feche e reabra o aplicativo. O aplicativo carrega a lista de reprodução do cache offline, enquanto a solicitação de rede é executada em segundo plano.

    Se novos dados vierem da rede, a tela será atualizada automaticamente para mostrar os novos dados. No entanto, o servidor DevBytes não atualiza seu conteúdo, portanto, você não vê a atualização dos dados.

Ótimo trabalho! Neste tutorial, você implementou um cache offline usando Room, anexou o cache a um repositório e manipulou LiveData usando uma transformação. Você também integrou o cache offline com o ViewModel para exibir a lista de reprodução do repositório em vez de buscar a lista de reprodução da rede.

Projeto Android Studio: DevBytesRepository

Documentação para desenvolvimento em Android:

Documentação de referência para o código inicial:

Esta seção lista as possíveis tarefas de casa para os alunos que estão trabalhando neste tutorial como parte de um curso ministrado por um instrutor.

Responda a essas perguntas

Pergunta 1

Qual componente nos Componentes de arquitetura do Android é responsável por manter o cache offline atualizado e obter dados da rede?

ViewModel

LiveData

▢ Repositório

▢ Atividades

Pergunta 2

Qual é a melhor maneira de salvar dados estruturados no sistema de arquivos do dispositivo para armazenamento em cache offline?

Room

▢ Arquivos

▢ SharedPreferences

▢ Cache do Retrofit

Pergunta 3

O Transformations.map converte um LiveData em outro _______.

ViewModel

LiveData

▢ Repositório

▢ Objeto DAO

Pergunta 4

Ao implementar o cache offline, qual das estratégias a seguir é um bom exemplo do conceito de separação de interesses?

▢ Crie classes separadas para representar os objetos de rede, domínio e banco de dados.

▢ Crie uma única classe para representar os objetos de rede, domínio e banco de dados.

▢ Crie uma única classe para representar os objetos de rede e domínio, e outra classe para representar o objeto de banco de dados.

▢ Crie uma única classe para representar o objeto de rede e outra classe para representar o banco de dados e os objetos de domínio.

Comece para a próxima lição: 09.2: WorkManager

Para obter enlaces para outros tutoriais neste curso, consulte a página de destino dos tutoriais Fundamentos de Android em Kotlin.