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.
Você deve estar familiarizado com:
Room
.ViewModel
, ViewModelFactory
e
LiveData
.LiveData
.Room
para criar um cache offline.Room
. ViewModel
. 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.
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.
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.
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
}
}
delay
(2000)
, que você adicionou em uma
etapa anterior.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.
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. network
.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.O resto da arquitetura dos aplicativos é semelhante aos outros aplicativos usados nos tutoriais anteriores:
network/Service.kt
, busca a lista de reprodução devbytes
da
rede. DevByteViewModel
mantém os dados do aplicativo como objetos LiveData
. DevByteFragment
, contém um RecyclerView
para exibir a lista
de vídeo e os observadores para os objetos LiveData
.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 |
|
Boa solução para solicitações e respostas simples, chamadas de rede não frequentes ou pequenos conjuntos de dados. |
Você pode usar |
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 |
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.
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"
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
.
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)
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.
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)
}
}
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.
database/Room.kt
, defina uma interface VideoDao
e anote com @Dao
.
@Dao
interface VideoDao {
}
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
.
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>)
Nesta etapa, você adiciona o banco de dados para seu cache offline implementando RoomDatabase
.
database/Room.kt
, após a interface VideoDao
, crie uma classe
abstract
chamada VideosDatabase
. Estenda VideosDatabase
de
RoomDatabase
.
@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
.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
}
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.
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 é 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.
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.
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) {
}
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.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.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) {
}
}
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()
refreshVideos()
, depois de buscar a lista de reprodução na rede, armazene a
lista de reprodução no banco de dados Room
.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())
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())
}
}
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.
VideosRepository
, declare um objeto LiveData
chamado
videos
para conter uma lista de DevByteVideo
objetos.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()
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.
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))
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
}
}
}
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()
}
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
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
Room
. A lista de reprodução é exibida na tela do banco de dados
Room
, não diretamente da rede.Ó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
Room
é uma biblioteca de mapeamento de objetos SQLite, o que
significa que fornece uma camada de abstração sobre SQLite. Usando o Room
é a melhor prática
recomendada para implementar o cache offline. Room
e serviços da web,
do resto do aplicativo. A classe do repositório fornece uma API limpa para acesso aos dados para o resto do
aplicativo. Room
.
Implemente um repositório para gerenciar e acessar o banco de dados da Room
. No
ViewModel
, busque e exiba os dados diretamente do repositório em vez de buscar os dados da rede.
LiveData
no DAO. Quando o banco de dados Room
é atualizado,
Room
executa a consulta em segundo plano para atualizar o LiveData
associado. Documentação para desenvolvimento em Android:
LiveData
Transformations.map
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.
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
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
O Transformations.map
converte um LiveData
em outro
_______.
▢ ViewModel
▢ LiveData
▢ Repositório
▢ Objeto DAO
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:
Para obter enlaces para outros tutoriais neste curso, consulte a página de destino dos tutoriais Fundamentos de Android em Kotlin.