1. Antes de começar
No codelab anterior, você aprendeu sobre os princípios básicos dos bancos de dados relacionais e sobre como ler e gravar dados usando os comandos SQL: SELECT, INSERT, UPDATE e DELETE. Saber trabalhar com bancos de dados relacionais é uma habilidade fundamental para ter em sua jornada de programação. Entender como os bancos de dados relacionais funcionam também é essencial para implementar a persistência de dados em um app Android. Você começará a fazer isso nesta lição.
Uma maneira fácil de usar um banco de dados em um app Android é com uma biblioteca chamada Room. O Room é uma biblioteca de mapeamento relacional de objeto (ORM, na sigla em inglês) que, como o nome indica, mapeia as tabelas em um banco de dados relacional em busca de objetos que podem ser usados no código Kotlin. Nesta lição, você se concentrará apenas em ler dados. Usando um banco de dados pré-preenchido, você carregará dados de uma tabela de horários de chegada de ônibus e os apresentará em uma RecyclerView
.
Nesse processo, você aprenderá os princípios básicos de como usar o Room, incluindo a classe de banco de dados, o DAO, as entidades e os modelos de visualização. Você também conhecerá a classe ListAdapter
, outra forma de apresentar dados em uma RecyclerView
, e um fluxo, recurso da linguagem Kotlin semelhante ao LiveData
, que permitirá que a IU responda a mudanças no banco de dados.
Pré-requisitos
- Familiaridade com a programação orientada a objetos e o uso de classes, objetos e herança em Kotlin.
- Conhecimentos básicos sobre bancos de dados relacionais e SQL, ensinados no codelab Princípios básicos de SQL.
- Experiência com corrotinas do Kotlin.
O que você aprenderá
Ao final desta lição, você saberá:
- representar tabelas de banco de dados como objetos Kotlin (entidades);
- definir a classe do banco de dados para usar o Room no app e pré-preencher um banco de dados com um arquivo;
- definir a classe DAO e usar consultas SQL para acessar o banco de dados com o código Kotlin;
- definir um modelo de visualização para permitir que a IU interaja com o DAO;
- usar um ListAdapter com uma visualização de reciclagem;
- os conceitos básicos do fluxo do Kotlin e como usá-lo para fazer com que a IU responda a mudanças nos dados.
O que você vai criar
- Você lerá dados de um banco de dados pré-preenchido com o Room e os apresentará em uma visualização de reciclagem em um app simples de horários de ônibus.
2. Começar
O nome do app com que você trabalhará neste codelab é Bus Schedule. Ele apresenta uma lista de pontos de ônibus e horários de chegada, do horário mais próximo ao mais tarde.
Ao tocar em uma linha na primeira tela, o app exibe uma nova tela, que mostra somente os próximos horários de chegada para o ponto de ônibus selecionado.
As informações sobre os pontos de ônibus vêm de um banco de dados pré-empacotado com o app. Entretanto, no estado atual, nada será exibido quando o app for executado pela primeira vez. Você integrará o Room para que o app exiba o banco de dados pré-preenchido com os tempos de chegada.
- Navegue até a página do repositório do GitHub fornecida para o projeto.
- Verifique se o nome da ramificação corresponde ao especificado no codelab. Por exemplo, na captura de tela a seguir, o nome da ramificação é main.
- Na página do GitHub do projeto, clique no botão Code, que vai mostrar uma janela pop-up.
- Na janela pop-up, clique no botão Download ZIP para salvar o projeto no seu computador. Aguarde a conclusão do download.
- Localize o arquivo no computador, que provavelmente está na pasta Downloads.
- Clique duas vezes para descompactar o arquivo ZIP. Isso cria uma nova pasta com os arquivos do projeto.
Abrir o projeto no Android Studio
- Inicie o Android Studio.
- Na janela Welcome to Android Studio, clique em Open.
Observação: caso o Android Studio já esteja aberto, selecione a opção File > Open.
- No navegador de arquivos, vá até a pasta descompactada do projeto, que provavelmente está na pasta Downloads.
- Clique duas vezes nessa pasta do projeto.
- Aguarde o Android Studio abrir o projeto.
- Clique no botão Run
para criar e executar o app. Confira se ele é criado da forma esperada.
3. Adicionar dependência do Room
Como acontece com qualquer outra biblioteca, primeiro você precisará adicionar as dependências necessárias para usar o Room no app Bus Schedule. Para isso, será necessário fazer duas pequenas mudanças, uma em cada arquivo do Gradle.
- No arquivo
build.gradle
do projeto, defina aroom_version
no bloco ext.
ext {
kotlin_version = "1.6.20"
nav_version = "2.4.1"
room_version = '2.4.2'
}
- No arquivo
build.gradle
do app, adicione as seguintes dependências ao final da lista de dependências.
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
- Sincronize as mudanças e crie o projeto para verificar se as dependências foram adicionadas corretamente.
Nas próximas páginas, você verá uma apresentação dos componentes necessários para integrar o Room em um app: os modelos, o DAO, os modelos de visualização e a classe de banco de dados.
4. Criar uma entidade
Ao aprender sobre bancos de dados relacionais no codelab anterior, você viu como os dados eram organizados em tabelas formadas por várias colunas, cada uma representando uma propriedade específica de um tipo de dado específico. Da mesma forma como as classes do Kotlin oferecem um modelo para cada objeto, uma tabela em um banco de dados fornece um modelo para cada item ou linha da tabela. Assim, uma classe do Kotlin pode ser usada para representar cada tabela do banco de dados.
Ao trabalhar com o Room, cada tabela é representada por uma classe. Em uma biblioteca de ORM (mapeamento relacional de objeto, na sigla em inglês) como o Room, essas classes costumam ser chamadas de classes de modelo ou entidades.
O banco de dados do app Bus Schedule é formado por uma única tabela, com informações básicas sobre os horários de chegada dos ônibus.
id
: um número inteiro que fornece um identificador exclusivo que serve como chave primáriastop_name
: uma stringarrival_time
: um número inteiro
Os tipos SQL usados no banco de dados são INTEGER
para Int
e TEXT
para String
. No entanto, ao trabalhar com o Room, você só precisa se preocupar com os tipos do Kotlin ao definir as classes de modelo. O mapeamento dos tipos de dados na classe de modelo para aqueles que serão usados no banco de dados é feito automaticamente.
Quando um projeto tem muitos arquivos, organize seus arquivos em pacotes diferentes para oferecer um controle de acesso melhor a cada classe e facilitar a localização de classes relacionadas. A fim de criar uma entidade para a tabela de horários, adicione um novo pacote com o nome database no pacote com.example.busschedule. Nesse pacote, adicione um novo pacote com o nome schedule para a entidade. Em seguida, no pacote database.schedule, crie um novo arquivo com o nome Schedule.kt e defina uma classe de dados com o nome Schedule
.
data class Schedule(
)
Conforme abordado na lição sobre os princípios básicos de SQL, as tabelas de dados precisam ter uma chave primária para identificar cada linha de forma exclusiva. A primeira propriedade a ser adicionada à classe Schedule
é um número inteiro para representar um ID exclusivo. Adicione uma nova propriedade e marque-a com a anotação @PrimaryKey
. Isso instrui o Room a tratar essa propriedade como a chave primária quando novas linhas forem inseridas.
@PrimaryKey val id: Int
Adicione uma coluna para o nome do ponto de ônibus. A coluna precisa ser do tipo String
. Para acrescentar novas colunas, será necessário adicionar uma anotação @ColumnInfo
para especificar um nome para a coluna. Normalmente, os nomes das colunas SQL têm palavras separadas por um sublinhado, diferentemente do formato lowerCamelCase usado pelas propriedades do Kotlin. O valor dessa coluna não pode ser nulo. Por isso, marque-o com a anotação @NonNull
.
@NonNull @ColumnInfo(name = "stop_name") val stopName: String,
Os horários de chegada são representados no banco de dados usando números inteiros. Trata-se de um carimbo de data/hora Unix (link em inglês), que pode ser convertido em uma data utilizável. Embora versões diferentes do SQL ofereçam formas distintas de converter datas, para os objetivos deste codelab, somente as funções de formato de data do Kotlin serão usadas. Adicione a seguinte coluna @NonNull
à classe de modelo.
@NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int
Por fim, para que o Room reconheça essa classe como algo que pode ser usado para definir tabelas do banco de dados, é necessário adicionar uma anotação à classe. Adicione @Entity
a uma linha separada, antes do nome da classe.
Por padrão, o Room usa o nome da classe como nome da tabela do banco de dados. Assim, o nome da tabela, conforme definido pela classe agora seria "Schedule". Como opção, também é possível especificar @Entity(tableName="schedule"). No entanto, como as consultas do Room não diferenciam maiúsculas de minúsculas, aqui é possível omitir explicitamente a definição de um nome de tabela em letras minúsculas.
Agora, a classe da entidade "schedule" vai ficar assim:
@Entity
data class Schedule(
@PrimaryKey val id: Int,
@NonNull @ColumnInfo(name = "stop_name") val stopName: String,
@NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int
)
5. Definir o DAO
A próxima classe que será adicionada para integrar o Room é o DAO. DAO significa objeto de acesso a dados, na sigla em inglês, e é uma classe do Kotlin que fornece acesso aos dados. Mais especificamente, o DAO é o local em que as funções de leitura e processamento de dados são incluídas. Chamar uma função no DAO equivale a executar um comando SQL no banco de dados. Funções DAO, como as que serão definidas neste app, geralmente especificam um comando SQL para que seja possível definir exatamente o que a função fará. O conhecimento sobre SQL adquirido no codelab anterior será útil ao definir o DAO.
- Adicione uma classe DAO à entidade "Schedule". No pacote database.schedule, crie um novo arquivo com o nome
ScheduleDao.kt
e defina uma interface com o nomeScheduleDao
. Assim como na classeSchedule
, é necessário adicionar uma anotação, desta vez@Dao
, para que a interface passe a ser utilizável com o Room.
@Dao
interface ScheduleDao {
}
- O app tem duas telas, e cada uma delas precisará de uma consulta diferente. A primeira tela exibe todas as paradas de ônibus em ordem crescente de horário de chegada. Nesse caso de uso, a consulta precisa apenas abranger todas as colunas e incluir uma cláusula
ORDER BY
adequada. A consulta é especificada como uma string transmitida para uma anotação@Query
. Defina uma funçãogetAll()
que retorna uma lista de objetosSchedule
, incluindo a anotação@Query
, conforme mostrado abaixo.
@Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
fun getAll(): List<Schedule>
- Na segunda consulta, você também selecionará todas as colunas da tabela de horários. No entanto, somente os resultados que correspondam ao nome da parada selecionada serão retornados. Para isso, é necessário adicionar uma cláusula
WHERE
. É possível referenciar valores do Kotlin na consulta inserindo dois pontos (:
) antes dela. Por exemplo,:stopName
para o parâmetro da função. Como antes, os resultados são organizados em ordem crescente por horário de chegada. Defina uma funçãogetByStopName()
que use um parâmetroString
chamadostopName
e retorne umaList
de objetosSchedule
com uma anotação@Query
, conforme mostrado.
@Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC")
fun getByStopName(stopName: String): List<Schedule>
6. Definir o ViewModel
Agora que o DAO foi configurado, tecnicamente você já tem tudo que é necessário para começar a acessar o banco de dados nos fragmentos. No entanto, embora isso funcione na teoria, geralmente não é considerado uma prática recomendada. Isso porque, em apps mais complexos, é provável que existam várias telas que acessam apenas uma parte específica dos dados. Embora o ScheduleDao
seja relativamente simples, é fácil imaginar como isso poderia fugir do controle ao trabalhar com duas ou mais telas diferentes. Por exemplo, um DAO pode ser parecido com este exemplo:
@Dao
interface ScheduleDao {
@Query(...)
getForScreenOne() ...
@Query(...)
getForScreenTwo() ...
@Query(...)
getForScreenThree()
}
Embora o código da "Screen 1" (tela 1) possa acessar getForScreenOne()
, não existe um bom motivo para ele acessar os outros métodos. Em vez disso, é recomendável separar a parte do DAO que é exposta à visualização em uma classe separada, chamada de modelo de visualização. Esse é um padrão de arquitetura comum em apps para dispositivos móveis. O uso de um modelo de visualização ajuda a definir uma separação clara entre o código da IU do app e o modelo de dados dele. Isso também ajuda a testar cada parte do código de forma independente. Esse assunto vai ser mais explorado ao longo da sua jornada de desenvolvimento para Android.
Usando um modelo de visualização, é possível aproveitar a classe ViewModel
. A ViewModel
é usada para armazenar dados relacionados à IU de um app. Ela também reconhece o ciclo de vida, o que significa que responde a eventos de ciclo de vida da mesma forma que uma atividade ou um fragmento. Se eventos de ciclo de vida, como a rotação da tela, fizerem com que uma atividade ou um fragmento seja destruído e recriado, o ViewModel
associado não precisará ser recriado. Como não é possível fazer isso acessando diretamente uma classe DAO, é recomendável usar a subclasse ViewModel
para fazer a separação entre a responsabilidade de carregar dados e a atividade ou o fragmento.
- Para criar uma classe de modelo de visualização, crie um novo arquivo chamado BusScheduleViewModel.kt em um novo pacote chamado viewmodels. Defina uma classe para o modelo de visualização. Ela precisa usar um único parâmetro do tipo
ScheduleDao
.
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
- Como esse modelo de visualização será usado por ambas as telas, será necessário adicionar um método para buscar todos os horários, bem como os horários filtrados por ponto de ônibus. Para fazer isso, chame os métodos correspondentes de
ScheduleDao
.
fun fullSchedule(): List<Schedule> = scheduleDao.getAll()
fun scheduleForStopName(name: String): List<Schedule> = scheduleDao.getByStopName(name)
Embora o modelo de visualização já tenha sido definido, não é possível instanciar somente um BusScheduleViewModel
diretamente e esperar que tudo funcione. Como o objetivo da classe BusScheduleViewModel
do ViewModel é ter reconhecimento de ciclo de vida, ela precisa ser instanciada por um objeto que possa responder a eventos do ciclo de vida. Se você instanciar a classe diretamente em um dos fragmentos, o objeto de fragmento vai ter que processar tudo, incluindo todo o gerenciamento de memória, que está fora do escopo do código do app. Em vez disso, você pode criar uma classe conhecida como factory para instanciar objetos de modelo de visualização para você.
- Para criar uma factory, abaixo da classe de modelo de visualização, crie uma nova classe
BusScheduleViewModelFactory
, herdada deViewModelProvider.Factory
.
class BusScheduleViewModelFactory(
private val scheduleDao: ScheduleDao
) : ViewModelProvider.Factory {
}
- Você precisará usar um código boilerplate para instanciar corretamente um modelo de visualização. Em vez de inicializar a classe diretamente, modifique o método
create()
que retorna umaBusScheduleViewModelFactory
e faz algumas verificações de erros. Implementecreate()
dentro da classeBusScheduleViewModelFactory
, conforme mostrado a seguir.
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(BusScheduleViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return BusScheduleViewModel(scheduleDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
Agora é possível instanciar um objeto BusScheduleViewModelFactory
com BusScheduleViewModelFactory.create()
, para que o modelo de visualização possa reconhecer o ciclo de vida sem que isso precise ser processado diretamente pelo fragmento.
7. Criar uma classe de banco de dados e pré-preencher o banco de dados
Agora que você já definiu os modelos, o DAO e um modelo de visualização para que os fragmentos acessem o DAO, ainda é necessário informar ao Room o que fazer com todas essas classes. É aí que entra a classe AppDatabase
. Um app Android que usa o Room, como é o caso do seu, tem uma subclasse da RoomDatabase
e algumas responsabilidades essenciais. No app, a AppDatabase
precisa:
- especificar quais entidades estão definidas no banco de dados;
- fornecer acesso a uma única instância de cada classe DAO;
- executar qualquer outra configuração, como pré-preencher o banco de dados.
Você pode estar se perguntando por que o Room não consegue encontrar todas as entidades e objetos DAO. Isso ocorre porque é provável que o app tenha vários bancos de dados ou inúmeros cenários. Dessa forma, a biblioteca não consegue presumir a intenção do desenvolvedor. A classe AppDatabase
oferece controle total sobre os modelos, classes DAO e qualquer configuração do banco de dados que você queira executar.
- Para adicionar uma classe
AppDatabase
, crie um novo arquivo chamadoAppDatabase.kt
no pacote database e defina uma nova classe abstrataAppDatabase
, herdada daRoomDatabase
.
abstract class AppDatabase: RoomDatabase() {
}
- A classe do banco de dados possibilita que outras acessem facilmente as classes DAO. Adicione uma função abstrata que retorne um
ScheduleDao
.
abstract fun scheduleDao(): ScheduleDao
- Ao usar uma classe
AppDatabase
, é importante garantir que exista apenas uma instância do banco de dados, para evitar disputas ou outros possíveis problemas. A instância fica armazenada no objeto complementar. Você também precisará de um método que retorne a instância existente ou crie o banco de dados pela primeira vez. Isso é definido no objeto complementar. Adicione ocompanion object
a seguir logo abaixo da funçãoscheduleDao()
.
companion object {
}
Em companion object
, adicione uma propriedade com o nome INSTANCE
do tipo AppDatabase
. Inicialmente, esse valor é definido como null
, portanto, o tipo é marcado com ?
. Também é marcado com uma anotação @Volatile
. Embora os detalhes sobre quando usar uma propriedade volátil sejam um pouco avançados para esta lição, você vai precisar usar esse tipo de propriedade na instância AppDatabase
para evitar possíveis bugs.
@Volatile
private var INSTANCE: AppDatabase? = null
Abaixo da propriedade INSTANCE
, defina uma função para retornar a instância AppDatabase
.
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database")
.createFromAsset("database/bus_schedule.db")
.build()
INSTANCE = instance
instance
}
}
Na implementação de getDatabase()
, use o operador Elvis para retornar a instância atual do banco de dados, se já existir, ou criar o banco de dados pela primeira vez, se necessário. Como os dados são preenchidos automaticamente neste app, Você também chama createFromAsset()
para carregar os dados existentes. O arquivo bus_schedule.db
pode ser encontrado no pacote assets.database
do projeto.
- Assim como as classes de modelo e o DAO, a classe do banco de dados requer uma anotação com algumas informações específicas. Todos os tipos de entidade estão listados em uma matriz, e cada tipo pode ser acessado usando
ClassName::class
. O banco de dados também recebe um número de versão, que será definido como 1. Adicione a anotação@Database
da seguinte maneira:
@Database(entities = arrayOf(Schedule::class), version = 1)
Agora que a classe AppDatabase
foi criada, há mais um passo para que ela passe a ser utilizável. Será necessário fornecer uma subclasse personalizada da classe Application
e criar uma propriedade lazy
que contenha o resultado de getDatabase()
.
- No pacote com.example.busschedule, adicione um novo arquivo com o nome
BusScheduleApplication.kt
e crie uma classeBusScheduleApplication
herdada da classeApplication
.
class BusScheduleApplication : Application() {
}
- Adicione uma propriedade de banco de dados do tipo
AppDatabase
. A propriedade precisa ser lenta e retornar o resultado da chamada degetDatabase()
na classeAppDatabase
.
class BusScheduleApplication : Application() {
val database: AppDatabase by lazy { AppDatabase.getDatabase(this) }
- Por fim, para garantir que a classe
BusScheduleApplication
seja usada, em vez da classe base padrãoApplication
, será necessário fazer uma pequena mudança no manifesto. EmAndroidMainifest.xml
, defina a propriedadeandroid:name
comocom.example.busschedule.BusScheduleApplication
.
<application
android:name="com.example.busschedule.BusScheduleApplication"
...
Agora o modelo do app está configurado. Você já pode começar a usar os dados do Room na IU. Nas próximas páginas, você criará um ListAdapter
para que a RecyclerView
do app exiba os dados dos horários de ônibus e responda a mudanças de dados de forma dinâmica.
8. Criar o ListAdapter
É hora de colocar todo esse trabalho em uso e conectar o modelo à visualização. Anteriormente, ao usar uma RecyclerView
, você usava um RecyclerView
.Adapter
para apresentar uma lista estática de dados. Embora isso certamente funcione para um app como o Bus Schedule, é comum processar mudanças em tempo real ao trabalhar com um banco de dados. Mesmo que o conteúdo de apenas um item mude, toda a visualização de reciclagem será atualizada. Isso não funcionará para a maioria dos apps que usam a persistência.
Uma alternativa que pode ser usada para uma lista que muda de forma dinâmica é chamada de ListAdapter
. O ListAdapter
usa uma AsyncListDiffer para determinar as diferenças entre uma lista de dados antiga e uma nova. Em seguida, a visualização de reciclagem será atualizada somente com base nas diferenças entre as duas listas. Como resultado, a visualização da reciclagem vai ser mais eficiente ao processar dados atualizados com frequência, como ocorre frequentemente em aplicativos com banco de dados.
Como a IU é idêntica nas duas telas, você só vai precisar criar um ListAdapter
que possa ser usado por ambas as telas.
- Crie um novo arquivo
BusStopAdapter.kt
e uma classeBusStopAdapter
, conforme mostrado abaixo. A classe estende umListAdapter
genérico que usa uma lista de objetosSchedule
e uma classeBusStopViewHolder
para a IU. Para oBusStopViewHolder
, você também precisa transmitir um tipoDiffCallback
, que será definido em breve. A própria classeBusStopAdapter
também recebe um parâmetro,onItemClicked()
. Essa função será usada para processar a navegação quando um item for selecionado na primeira tela. Porém, na segunda tela, apenas uma função vazia será transmitida.
class BusStopAdapter(private val onItemClicked: (Schedule) -> Unit) : ListAdapter<Schedule, BusStopAdapter.BusStopViewHolder>(DiffCallback) {
}
- De forma semelhante a um adaptador de visualização de reciclagem, você precisará de um fixador de visualização para acessar as visualizações criadas a partir do arquivo de layout no código. O layout das células já foi criado. Basta criar uma classe
BusStopViewHolder
, conforme mostrado abaixo, e implementar a funçãobind()
para definir o texto destopNameTextView
como o nome do ponto de ônibus e o texto daarrivalTimeTextView
como a data formatada.
class BusStopViewHolder(private var binding: BusStopItemBinding): RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SimpleDateFormat")
fun bind(schedule: Schedule) {
binding.stopNameTextView.text = schedule.stopName
binding.arrivalTimeTextView.text = SimpleDateFormat(
"h:mm a").format(Date(schedule.arrivalTime.toLong() * 1000)
)
}
}
- Modifique e implemente o
onCreateViewHolder()
, infle o layout e instrua oonClickListener()
a chamaronItemClicked()
para o item na posição atual.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BusStopViewHolder {
val viewHolder = BusStopViewHolder(
BusStopItemBinding.inflate(
LayoutInflater.from( parent.context),
parent,
false
)
)
viewHolder.itemView.setOnClickListener {
val position = viewHolder.adapterPosition
onItemClicked(getItem(position))
}
return viewHolder
}
- Modifique e implemente o
onBindViewHolder()
e vincule a visualização na posição especificada.
override fun onBindViewHolder(holder: BusStopViewHolder, position: Int) {
holder.bind(getItem(position))
}
- Você se lembra da classe
DiffCallback
que foi especificada para oListAdapter
? Ela é apenas um objeto que ajuda oListAdapter
a determinar quais itens nas listas novas e antigas são diferentes ao atualizar uma lista. Há dois métodos:areItemsTheSame()
verifica se o objeto (ou a linha no banco de dados no seu caso) é igual apenas verificando o ID. OareContentsTheSame()
verifica se todas as propriedades, não apenas o ID, são iguais. Esses métodos permitem que oListAdapter
determine quais itens foram inseridos, atualizados e excluídos para que a IU possa ser atualizada de acordo.
Adicione um objeto complementar e implemente DiffCallback
, conforme mostrado a seguir.
companion object {
private val DiffCallback = object : DiffUtil.ItemCallback<Schedule>() {
override fun areItemsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
return oldItem == newItem
}
}
}
A configuração do adaptador está pronta. Ele será usado nas duas telas do app.
- Primeiro, em
FullScheduleFragment.kt
, é necessário ter uma referência ao modelo de visualização.
private val viewModel: BusScheduleViewModel by activityViewModels {
BusScheduleViewModelFactory(
(activity?.application as BusScheduleApplication).database.scheduleDao()
)
}
- Em seguida, em
onViewCreated()
, adicione o código a seguir para configurar a visualização de reciclagem e atribuir o gerenciador de layout.
recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(requireContext())
- Depois, atribua a propriedade do adaptador. A ação transmitida usará o
stopName
para navegar pela próxima tela selecionada, de modo que a lista de pontos de ônibus possa ser filtrada.
val busStopAdapter = BusStopAdapter({
val action = FullScheduleFragmentDirections.actionFullScheduleFragmentToStopScheduleFragment(
stopName = it.stopName
)
view.findNavController().navigate(action)
})
recyclerView.adapter = busStopAdapter
- Por fim, para atualizar uma visualização em lista, chame
submitList()
, transmitindo a lista de pontos de ônibus do modelo de visualização.
// submitList() is a call that accesses the database. To prevent the
// call from potentially locking the UI, you should use a
// coroutine scope to launch the function. Using GlobalScope is not
// best practice, and in the next step we'll see how to improve this.
GlobalScope.launch(Dispatchers.IO) {
busStopAdapter.submitList(viewModel.fullSchedule())
}
- Faça o mesmo no
StopScheduleFragment
. Primeiro, encontre uma referência ao modelo de visualização.
private val viewModel: BusScheduleViewModel by activityViewModels {
BusScheduleViewModelFactory(
(activity?.application as BusScheduleApplication).database.scheduleDao()
)
}
- Em seguida, configure a visualização de reciclagem em
onViewCreated()
. Desta vez, basta transmitir um bloco vazio (função) com{}
. O objetivo é que nada aconteça ao tocar nas linhas dessa tela.
recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(requireContext())
val busStopAdapter = BusStopAdapter({})
recyclerView.adapter = busStopAdapter
// submitList() is a call that accesses the database. To prevent the
// call from potentially locking the UI, you should use a
// coroutine scope to launch the function. Using GlobalScope is not
// best practice, and in the next step we'll see how to improve this.
GlobalScope.launch(Dispatchers.IO) {
busStopAdapter.submitList(viewModel.scheduleForStopName(stopName))
}
- Agora que você configurou o adaptador, a integração do Room ao app Bus Schedule está pronta. Execute o app, e uma lista de horários de chegada será exibida. Toque em uma linha para acessar a tela de detalhes.
9. Responder a mudanças de dados usando o fluxo
Embora a visualização em lista esteja configurada para processar as mudanças de dados de forma eficiente sempre que submitList()
for chamado, o app ainda não processa atualizações dinâmicas. Para ver como isso ocorre, abra o Database Inspector e execute a consulta a seguir para inserir um novo item na tabela de horários.
INSERT INTO schedule
VALUES (null, 'Winding Way', 1617202500)
Você perceberá que nada acontece no emulador. O usuário presumirá que os dados não mudaram. Será necessário executar o app novamente para ver as mudanças.
O problema é que List<Schedule>
é retornado de cada uma das funções DAO apenas uma vez. Mesmo que os dados subjacentes sejam atualizados, submitList()
não será chamado para atualizar a IU. Assim, da perspectiva do usuário, não haverá mudanças.
Para corrigir isso, use um recurso do Kotlin chamado fluxo assíncrono, frequentemente conhecido apenas como fluxo. Ele permitirá que o DAO emita dados de forma contínua com base no banco de dados. Se um item for inserido, atualizado ou excluído, o resultado será enviado de volta ao fragmento. Usando uma função chamada collect(),
, é possível chamar submitList()
com o novo valor emitido pelo fluxo, para que o ListAdapter
possa atualizar a IU com base nos novos dados.
- Para usar o fluxo no Bus Schedule, abra
ScheduleDao.kt
. Para converter as funções DAO para retornar umFlow
, basta mudar o tipo de retorno da funçãogetAll()
paraFlow<List<Schedule>>
.
fun getAll(): Flow<List<Schedule>>
- Da mesma forma, atualize o valor de retorno da função
getByStopName()
.
fun getByStopName(stopName: String): Flow<List<Schedule>>
- As funções do modelo de visualização que acessam a classe DAO também precisam ser atualizadas. Atualize os valores de retorno de
Flow<List<Schedule>>
parafullSchedule()
escheduleForStopName()
.
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
fun fullSchedule(): Flow<List<Schedule>> = scheduleDao.getAll()
fun scheduleForStopName(name: String): Flow<List<Schedule>> = scheduleDao.getByStopName(name)
}
- Por fim, em
FullScheduleFragment.kt
, obusStopAdapter
precisa ser atualizado quandocollect()
é chamado nos resultados da consulta. Como o métodofullSchedule()
é uma função de suspensão, ele precisa ser chamado em uma corrotina. Substitua a linha:
busStopAdapter.submitList(viewModel.fullSchedule())
Pelo seguinte código que usa o fluxo retornado de fullSchedule()
:
lifecycle.coroutineScope.launch {
viewModel.fullSchedule().collect() {
busStopAdapter.submitList(it)
}
}
- Faça o mesmo em
StopScheduleFragment
, mas substitua a chamada porscheduleForStopName()
usando o código a seguir.
lifecycle.coroutineScope.launch {
viewModel.scheduleForStopName(stopName).collect() {
busStopAdapter.submitList(it)
}
}
- Depois de fazer as mudanças acima, execute novamente o app para verificar se as mudanças nos dados agora são processadas em tempo real. Quando o app estiver em execução, retorne ao Database Inspector e envie a seguinte consulta para inserir um novo horário de chegada antes das 8h.
INSERT INTO schedule
VALUES (null, 'Winding Way', 1617202500)
O novo item aparecerá no topo da lista.
O app Bus Schedule está pronto. Ótimo trabalho até aqui! Agora você tem uma base sólida para trabalhar com o Room. No próximo módulo, você estudará mais sobre o Room com um novo app de exemplo e aprenderá a salvar dados criados pelo usuário em um dispositivo.
10. Código da solução
O código da solução para este codelab está no projeto e no módulo mostrados abaixo.
- Navegue até a página do repositório do GitHub fornecida para o projeto.
- Verifique se o nome da ramificação corresponde ao especificado no codelab. Por exemplo, na captura de tela a seguir, o nome da ramificação é main.
- Na página do GitHub do projeto, clique no botão Code, que vai mostrar uma janela pop-up.
- Na janela pop-up, clique no botão Download ZIP para salvar o projeto no seu computador. Aguarde a conclusão do download.
- Localize o arquivo no computador, que provavelmente está na pasta Downloads.
- Clique duas vezes para descompactar o arquivo ZIP. Isso cria uma nova pasta com os arquivos do projeto.
Abrir o projeto no Android Studio
- Inicie o Android Studio.
- Na janela Welcome to Android Studio, clique em Open.
Observação: caso o Android Studio já esteja aberto, selecione a opção File > Open.
- No navegador de arquivos, vá até a pasta descompactada do projeto, que provavelmente está na pasta Downloads.
- Clique duas vezes nessa pasta do projeto.
- Aguarde o Android Studio abrir o projeto.
- Clique no botão Run
para criar e executar o app. Confira se ele é criado da forma esperada.
11. Parabéns
Resumindo:
- As tabelas em um banco de dados SQL são representadas no Room por classes do Kotlin chamadas entidades.
- O DAO oferece métodos correspondentes aos comandos SQL, que interagem com o banco de dados.
- O
ViewModel
é um componente com reconhecimento de ciclo de vida usado para separar os dados do app da visualização. - A classe
AppDatabase
informa o Room quais entidades usar, oferece acesso ao DAO e executa todas as configurações ao criar o banco de dados. - O
ListAdapter
é um adaptador usado com aRecyclerView
, ideal para processar listas atualizadas de forma dinâmica. - Fluxo é um recurso do Kotlin para retornar um stream de dados e pode ser usado com o Room para garantir que a IU e o banco de dados estejam sincronizados.