O objetivo dos Componentes da arquitetura é fornecer orientações sobre a arquitetura de apps, com bibliotecas para tarefas comuns, como o gerenciamento do ciclo de vida e a persistência de dados. Os Componentes da arquitetura ajudam você a estruturar o app de maneira robusta, testável e de fácil manutenção com menos código boilerplate. As bibliotecas de Componentes da Arquitetura fazem parte do Android Jetpack.
Esta é a versão em Kotlin do codelab. A versão na linguagem de programação Java pode ser encontrada neste link.
Se você encontrar algum problema (bugs no código, erros gramaticais, instruções pouco claras, etc.) neste codelab, informe o problema no link Informar um erro no canto inferior esquerdo do codelab.
Pré-requisitos
É necessário conhecer o Kotlin, os conceitos de projeto orientados a objetos e os princípios básicos de desenvolvimento do Android, principalmente:
RecyclerView
e adaptadores.- Banco de dados SQLite e a linguagem de consulta SQLite.
- Corrotinas básicas. Se você não estiver familiarizado com as corrotinas, consulte Como usar corrotinas Kotlin no app Android.
Também é importante conhecer os padrões de arquitetura de software que separam os dados da interface do usuário, como o MVP ou MVC. Este codelab implementa a arquitetura definida no Guia para a arquitetura do app.
Este codelab é focado nos Componentes da arquitetura do Android. Conceitos e códigos não relacionados a este tópico são fornecidos para que você os copie e cole.
Se você não estiver familiarizado com o Kotlin, consulte uma versão deste codelab fornecida na linguagem de programação Java neste link.
O que você aprenderá
Neste codelab, você aprenderá a projetar e construir um app usando os Componentes da arquitetura Room, ViewModel e LiveData, além de criar um app que faz o seguinte:
- implementa nossa arquitetura recomendada usando os Componentes da arquitetura do Android;
- funciona com um banco de dados para obter e salvar os dados, além de pré-preencher o banco de dados com algumas palavras;
- Exibe todas as palavras em um
RecyclerView
noMainActivity
. - abre uma segunda atividade quando o usuário toca no botão "+". Quando o usuário insere uma palavra, ela é adicionada ao banco de dados e à lista.
O app é simples, mas suficientemente complexo para ser usado como modelo. Veja alguns exemplos:
Pré-requisitos
- Android Studio 3.0 ou mais recente e conhecimento sobre como usá-lo. Verifique se o Android Studio está atualizado, assim como o SDK e o Gradle.
- Um dispositivo ou emulador Android.
Este codelab disponibiliza todo o código necessário para que você crie o app completo.
Há muitas etapas para usar os Componentes da arquitetura e implementar a arquitetura recomendada. O mais importante é criar um modelo mental do que está acontecendo e entender como as peças se encaixam e como os dados fluem. Tente entender esses funcionamentos internos durante o codelab. Não simplesmente copie e cole o código.
Quais são os Componentes da arquitetura recomendados?
Para apresentar a terminologia, veja uma breve introdução aos Componentes da arquitetura e como eles funcionam juntos. Este codelab se concentra em um subconjunto dos componentes, nesse caso, LiveData, ViewModel e Room. Cada componente é explicado com mais detalhes à medida que é usado.
Este diagrama mostra uma forma básica da arquitetura:
Entidade: classe com anotação que descreve uma tabela de banco de dados ao trabalhar com a Room.
Banco de dados SQLite: armazenamento no dispositivo. A biblioteca de persistência Room cria e mantém esse banco de dados para você.
DAO: objeto de acesso a dados. Um mapeamento de consultas SQL para funções. Ao usar um DAO, você chama os métodos e a Room faz o resto.
Banco de dados da Room: simplifica o trabalho com o banco de dados e serve como ponto de acesso para o banco de dados SQLite (oculta SQLiteOpenHelper)
. O banco de dados da Room usa o DAO para realizar consultas ao banco de dados SQLite.
Repositório: uma classe que você cria e que é usada principalmente para gerenciar várias fontes de dados.
ViewModel: atua como um centro de comunicação entre o repositório (dados) e a IU. A IU não precisa mais se preocupar com a origem dos dados. As instâncias do ViewModel sobrevivem à recriação de atividade/fragmento.
LiveData: uma classe armazenadora de dados que pode ser observada. Ela sempre mantém/armazena em cache a versão mais recente dos dados e notifica os observadores quando os dados mudam. LiveData
é compatível com o ciclo de vida. Os componentes da IU apenas observam dados relevantes e não interrompem nem retomam a observação. O LiveData gerencia tudo isso automaticamente, já que conta com reconhecimento das mudanças relevantes do status do ciclo de vida durante a observação.
Visão geral da arquitetura do RoomWordSample
O diagrama a seguir mostra todas as partes do app. Cada uma das caixas (exceto a do banco de dados SQLite) representa uma classe que você criará neste codelab.
- Abra o Android Studio e clique em Start a new Android Studio project.
- Na janela Create New Project, escolha Empty Activity e clique em Next.
- Na tela seguinte, dê o nome RoomWordSample ao app e clique em Finish.
Em seguida, será necessário adicionar as bibliotecas de componentes aos arquivos do Gradle.
- No Android Studio, clique na guia "Projects" e expanda a pasta "Gradle Scripts".
Abra build.gradle
(Module: app).
- Aplique o plug-in do Kotlin para
kapt
processar anotações (link em inglês) adicionando-o após os outros plug-ins definidos na parte superior do arquivobuild.gradle
(Module: app).
apply plugin: 'kotlin-kapt'
- Adicione o bloco
packagingOptions
dentro do blocoandroid
para excluir o módulo de funções atômicas (links em inglês) do pacote e evitar avisos.
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
- Adicione o seguinte código no fim do bloco
dependencies
.
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
- No seu arquivo
build.gradle
(Project: RoomWordsSample), adicione os números de versão ao final do arquivo, conforme mostrado no código abaixo.
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}
Os dados desse app são palavras. Você precisará de uma tabela simples para conter esses valores:
O Room permite criar tabelas usando uma Entidade. Faça isso agora.
- Crie um novo arquivo de classe do Kotlin com o nome
Word
contendo a classe de dadosWord
(link em inglês).
Esta classe descreve a entidade (que representa a tabela SQLite) das palavras. Cada propriedade na classe representa uma coluna na tabela. A Room usará estas propriedades para criar a tabela e instanciar objetos de linhas no banco de dados.
Veja o código:
data class Word(val word: String)
Para tornar a classe Word
significativa para um banco de dados da Room, é necessário a anotar. As anotações identificam como cada parte dessa classe se relaciona a uma entrada no banco de dados. O Room usa essas informações para gerar o código.
Se você mesmo digitar as anotações (em vez de colá-las), o Android Studio importará automaticamente as classes de anotação.
- Atualize sua classe
Word
com anotações conforme mostrado neste código:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
Vejamos o que essas anotações fazem:
@Entity(tableName =
"word_table"
)
Cada classe@Entity
representa uma tabela SQLite. Anote a declaração da classe para indicar que é uma entidade. Você pode especificar o nome da tabela se quiser que seja diferente do nome da classe. Dessa forma, a tabela terá o nome "word_table".@PrimaryKey
Toda entidade precisa de uma chave primária. Para simplificar, cada palavra funciona como a própria chave primária.@ColumnInfo(name =
"word"
)
Especifica o nome da coluna na tabela se você quiser que seja diferente do nome da variável de membro. Dessa forma, a coluna terá o nome "word".- Todas as propriedades armazenadas no banco de dados precisam ter visibilidade pública, que é o padrão do Kotlin.
Você pode ver uma lista completa de anotações na referência do resumo do pacote da Room.
O que é o DAO?
No DAO (objeto de acesso a dados), você especifica consultas SQL e as associa a chamadas de método. O compilador verifica o SQL e gera consultas usando as anotações de conveniência para consultas comuns, como @Insert
. A Room usa o DAO para criar uma API limpa para seu código.
O DAO precisa ser uma interface ou uma classe abstrata.
Por padrão, todas as consultas precisam ser executadas em uma linha de execução separada.
O Room tem compatibilidade com corrotinas, permitindo anotar as consultas com o modificador suspend
e depois chamá-las em uma corrotina ou em outra função de suspensão.
Implementar o DAO
Vamos criar um DAO que fornece consultas para:
- ordenar todas as palavras em ordem alfabética;
- inserir uma palavra;
- excluir todas as palavras.
- Crie um novo arquivo de classe do Kotlin com o nome
WordDao
. - Copie e cole o código a seguir no
WordDao
e corrija as importações conforme necessário para que ele seja compilado.
@Dao
interface WordDao {
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): List<Word>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
Veja como fazer isso:
WordDao
é uma interface. Os DAOs precisam ser interfaces ou classes abstratas.- A anotação
@Dao
a identifica como uma classe DAO para a Room. suspend fun insert(word: Word)
: declara uma função de suspensão para inserir uma palavra.- A anotação
@Insert
é um método especial de DAO em que você não precisa fornecer nenhum SQL. Há também anotações@Delete
e@Update
para excluir e atualizar linhas, mas elas não serão usadas neste app. onConflict = OnConflictStrategy.IGNORE
: a estratégia onConflict selecionada ignora uma nova palavra se ela for exatamente igual à que já está na lista. Para saber mais sobre as estratégias de conflito disponíveis, confira a documentação.suspend fun deleteAll()
: declara uma função de suspensão para excluir todas as palavras.- Não há uma anotação de conveniência para excluir várias entidades. Por isso, a anotação genérica
@Query
é usada. @Query
("DELETE FROM word_table")
:@Query
requer que você forneça uma consulta SQL como um parâmetro de string à anotação, permitindo consultas de leitura complexas e outras operações.fun getAlphabetizedWords(): List<Word>
: um método para acessar todas as palavras e retornar umaList
deWords
.@Query(
"SELECT * from word_table ORDER BY word ASC"
)
: consulta que retorna uma lista de palavras classificadas em ordem crescente.
Quando os dados são alterados, convém realizar alguma ação, como exibir a atualização na IU. Isso significa que você precisa observar os dados para que possa reagir quando eles mudarem.
Dependendo da forma como os dados são armazenados, isso pode ser complicado. Observar mudanças em dados de vários componentes do app pode criar caminhos de dependência rígidos e explícitos entre eles. Isso dificulta os testes e a depuração, entre outras coisas.
LiveData
, uma classe de biblioteca de ciclo de vida para observação de dados, resolve esse problema. Use um valor de retorno do tipo LiveData
na descrição do método e a Room gerará todo o código necessário para atualizar o LiveData
quando o banco de dados for atualizado.
Em WordDao
, mude a assinatura do método getAlphabetizedWords()
para que o List<Word>
retornado seja agrupado com LiveData
.
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): LiveData<List<Word>>
Mais adiante neste codelab, você vai acompanhar as mudanças de dados usando um Observer
em MainActivity
.
O que é um banco de dados da Room?
- A Room é uma camada do banco de dados de um banco de dados SQLite.
- A Room cuida das tarefas de rotina que você costumava realizar com um
SQLiteOpenHelper
. - A Room usa o DAO para emitir consultas ao banco de dados.
- Por padrão, para evitar mau desempenho da IU, a Room não permite que você faça consultas na linha de execução principal. Quando as consultas da Room retornam o
LiveData
, elas são executadas automaticamente de forma assíncrona em uma linha de execução em segundo plano. - A Room oferece verificações das instruções do SQLite durante o tempo de compilação.
Implementar o banco de dados da Room
A classe do banco de dados da Room precisa ser abstrata e estender RoomDatabase
. Normalmente, você só precisa de uma instância de um banco de dados da Room para todo o app.
Vamos criar uma agora.
- Crie um arquivo de classe do Kotlin com o nome
WordRoomDatabase
e adicione este código a ele:
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
Vamos analisar o código:
- A classe de banco de dados da Room precisa ser
abstract
e estenderRoomDatabase
. - Use
@Database
para anotar a classe como um banco de dados da Room e usar os parâmetros de anotação para declarar as entidades que pertencem ao banco de dados e definir o número da versão. Cada entidade corresponde a uma tabela que será criada no banco de dados. As migrações de banco de dados estão fora do escopo deste codelab, por isso, definimosexportSchema
como falso para evitar um aviso de compilação. Em um app real, considere configurar um diretório para a Room usar e exportar o esquema para que você possa verificar o esquema atual no seu sistema de controle de versões. - O banco de dados expõe DAOs usando um método "getter" abstrato para cada @Dao.
- Definimos um Singleton,
WordRoomDatabase,
para evitar que várias instâncias do banco de dados sejam abertas ao mesmo tempo. getDatabase
retorna o Singleton. Ele criará o banco de dados na primeira vez que for acessado usando o builder do banco de dados da Room para criar um objetoRoomDatabase
no contexto do aplicativo da classeWordRoomDatabase
e o nomeará como"word_database"
.
O que é um repositório?
Uma classe de repositório abstrai o acesso a várias fontes de dados. O repositório não faz parte das bibliotecas dos Componentes da arquitetura, mas é uma prática recomendada para a separação e arquitetura do código. Uma classe de repositório fornece uma API limpa para acesso aos dados no restante do aplicativo.
Por que usar um repositório?
Um repositório gerencia consultas e permite usar vários back-ends. No exemplo mais comum, 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.
Como implementar o repositório
Crie um arquivo de classe do Kotlin chamado WordRepository
e cole o código a seguir nele:
// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {
// Room executes all queries on a separate thread.
// Observed LiveData will notify the observer when the data has changed.
val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
Veja as principais vantagens:
- O DAO é transmitido ao construtor do repositório, e não ao banco de dados inteiro. Isso ocorre porque só é necessário acessar o DAO, já que ele contém todos os métodos de leitura/gravação do banco de dados. Não é necessário expor todo o banco de dados ao repositório.
- A lista de palavras é uma propriedade pública. Ela é inicializada com a lista de palavras do
LiveData
da Room. Podemos fazer isso devido à forma como definimos o métodogetAlphabetizedWords
para retornar umLiveData
na etapa "quo";da classe LiveData". A Room executa todas as consultas em uma linha de execução separada. Em seguida, oLiveData
notificará o observador na linha de execução principal quando os dados tiverem mudado. - O modificador
suspend
informa ao compilador que ele precisa ser chamado por uma corrotina ou outra função de suspensão.
O que é um ViewModel?
O papel de um ViewModel
é fornecer dados à IU e sobreviver às mudanças de configuração. Um ViewModel
é um centro de comunicação entre o repositório e a IU. Também é possível usar um ViewModel
para compartilhar dados entre fragmentos. O ViewModel faz parte da biblioteca do ciclo de vida.
Para ver um guia introdutório sobre esse assunto, consulte ViewModel Overview
ou a postagem do blog ViewModels: um exemplo simples (em inglês).
Por que usar um ViewModel?
Um ViewModel
armazena os dados da IU do app de uma maneira que considera o ciclo de vida para sobreviver a mudanças na configuração. Separar os dados da IU do seu app das classes Activity
e Fragment
permite que você siga melhor o princípio de responsabilidade exclusiva: suas atividades e fragmentos são responsáveis por desenhar dados na tela, enquanto o ViewModel
pode cuidar do armazenamento e processamento de todos os dados necessários para a IU.
Na ViewModel
, use LiveData
para dados alternáveis que a IU usará ou exibirá. O uso de LiveData
tem vários benefícios:
- Você pode colocar um observador nos dados em vez de pesquisar mudanças e só atualizar a
IU quando os dados realmente mudarem. - O repositório e a IU são completamente separados por
ViewModel
. - Não há chamadas de banco de dados do
ViewModel
(tudo gerenciado no repositório), o que torna o código mais testável.
viewModelScope
Em Kotlin, todas as corrotinas são executadas em um CoroutineScope
(link em inglês). Um escopo controla o ciclo de vida das corrotinas com o job. Quando você cancelar o job de um escopo, todas as corrotinas iniciadas nesse escopo serão canceladas.
A biblioteca lifecycle-viewmodel-ktx
do AndroidX adiciona uma viewModelScope
como uma função de extensão da classe ViewModel
, permitindo usar escopos.
Para saber mais sobre como trabalhar com corrotinas no ViewModel, confira a Etapa 5 do codelab Como usar corrotinas Kotlin no seu app Android ou a postagem do blog Corrotinas fáceis no Android: viewModelScope (em inglês).
Implementar o ViewModel
Crie um arquivo de classe do Kotlin para WordViewModel
e adicione este código a ele:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}
Nesse código:
- foi criada uma classe com o nome
WordViewModel
que recebe oApplication
como parâmetro e estendeAndroidViewModel
. - Adição de uma variável de membro particular para manter uma referência ao repositório.
- adicionamos uma variável de membro
LiveData
pública para armazenar a lista de palavras em cache; - Criou um bloco
init
que recebe uma referência aoWordDao
doWordRoomDatabase
. - No bloco
init
, construímos oWordRepository
com base noWordRoomDatabase
. - No bloco
init
, o LiveDataallWords
foi inicializado usando o repositório. - criamos um método
insert()
wrapper que chama o métodoinsert()
do repositório. Dessa forma, a implementação deinsert()
é encapsulada na IU. Não queremos que a inserção bloqueie a linha de execução principal, por isso, estamos iniciando uma nova corrotina e chamando o repositório do repositório, que é uma função de suspensão. Como mencionado, os ViewModels têm um escopo de corrotina baseado no ciclo de vida deles que é usado nesse exemplo, com o nomeviewModelScope
;
Em seguida, você precisará adicionar o layout XML à lista e aos itens.
Este codelab presume que você está familiarizado com a criação de layouts em XML, então estamos apenas fornecendo o código.
Faça com que o material do tema do seu aplicativo defina o AppTheme
pai como Theme.MaterialComponents.Light.DarkActionBar
. Adicione um estilo aos itens da lista em values/styles.xml
:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!-- The default font for RecyclerView items is too small.
The margin is a simple delimiter between the words. -->
<style name="word_title">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_marginBottom">8dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:background">@android:color/holo_orange_light</item>
<item name="android:textAppearance">@android:style/TextAppearance.Large</item>
</style>
</resources>
Adicione um layout layout/recyclerview_item.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
style="@style/word_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
Em layout/activity_main.xml
, substitua a TextView
por uma RecyclerView
e adicione um botão de ação flutuante (FAB). Agora, seu layout ficará assim:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
tools:listitem="@layout/recyclerview_item"
android:padding="@dimen/big_padding"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"/>
</androidx.constraintlayout.widget.ConstraintLayout>
A aparência do FABc deve corresponder à ação disponível. Por isso, substituiremos o ícone por um símbolo de '+'.
Primeiro, precisamos adicionar um novo recurso de vetor:
- Selecione File > New > Vector Asset.
- Clique no ícone do robô do Android no campo Clip Art:.
- Pesquise "add" e selecione o recurso '+' Clique em OK.
- Depois disso, clique em Next.
- Confirme se o caminho do ícone é
main > drawable
e clique em Finish para adicionar o recurso. - Ainda em
layout/activity_main.xml
, atualize o FAB para incluir o novo drawable:
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"
android:src="@drawable/ic_add_black_24dp"/>
Você exibirá os dados em uma RecyclerView
, que é um pouco melhor do que simplesmente gerar os dados em uma TextView
. Este codelab pressupõe que você sabe como RecyclerView
, RecyclerView.LayoutManager
, RecyclerView.ViewHolder
e RecyclerView.Adapter
funcionam.
A variável words
no adaptador armazena os dados em cache. Na próxima tarefa, você adicionará o código que atualiza os dados automaticamente.
Crie um arquivo de classe do Kotlin para o WordListAdapter
que estenda o RecyclerView.Adapter
. Veja o código:
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}
Adicione a RecyclerView
ao método onCreate()
da MainActivity
.
No método onCreate()
após setContentView
:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
Execute o app para verificar se tudo está funcionando. Não haverá itens, porque os dados ainda não foram coletados.
Não há dados no banco de dados. Você os adicionará de duas maneiras: alguns dados quando o banco de dados for aberto e com uma Activity
para adicionar palavras.
Para excluir todo o conteúdo e preencher novamente o banco de dados sempre que o app for iniciado, crie um RoomDatabase.Callback
e substitua onOpen()
. Como não é possível realizar operações de banco de dados da Room na linha de execução de IU, onOpen()
inicia uma corrotina no agente de E/S.
Para iniciar uma corrotina, precisamos de um CoroutineScope
. Atualize o método getDatabase
da classe WordRoomDatabase
para receber também um escopo de corrotina como parâmetro:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
Atualize o inicializador de recuperação de banco de dados no bloco init
de WordViewModel
para também transmitir o escopo:
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
No WordRoomDatabase
, criamos uma implementação personalizada do RoomDatabase.Callback()
, que também recebe um CoroutineScope
como parâmetro do construtor. Em seguida, substituimos o método onOpen
para preencher o banco de dados.
Veja o código para criar o callback na classe WordRoomDatabase
:
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.wordDao())
}
}
}
suspend fun populateDatabase(wordDao: WordDao) {
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
}
}
Por fim, adicione o callback à sequência de compilação do banco de dados antes de chamar .build()
no Room.databaseBuilder()
:
.addCallback(WordDatabaseCallback(scope))
O código final ficará assim:
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
var wordDao = database.wordDao()
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
word = Word("TODO!")
wordDao.insert(word)
}
}
}
}
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
)
.addCallback(WordDatabaseCallback(scope))
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
Adicione estes recursos de string em values/strings.xml
:
<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>
Adicione este recurso de cor em value/colors.xml
:
<color name="buttonLabel">#FFFFFF</color>
Crie um novo arquivo de recursos de dimensão:
- Clique no módulo de app na janela Project.
- Selecione File > New > Android Resource File.
- Nos qualificadores disponíveis, selecione Dimension .
- Defina o nome do arquivo: dimens
Adicione estes recursos de dimensão a values/dimens.xml
:
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
Crie uma nova Activity
vazia do Android usando o modelo Empty Activity:
- Selecione File > New > Activity > Empty Activity.
- Insira
NewWordActivity
como o nome da atividade. - Verifique se a nova atividade foi adicionada ao manifesto do Android.
<activity android:name=".NewWordActivity"></activity>
Atualize o arquivo activity_new_word.xml
na pasta do layout com o seguinte código:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit_word"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/min_height"
android:fontFamily="sans-serif-light"
android:hint="@string/hint_word"
android:inputType="textAutoComplete"
android:layout_margin="@dimen/big_padding"
android:textSize="18sp" />
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="@string/button_save"
android:layout_margin="@dimen/big_padding"
android:textColor="@color/buttonLabel" />
</LinearLayout>
Atualize o código da atividade:
class NewWordActivity : AppCompatActivity() {
private lateinit var editWordView: EditText
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
editWordView = findViewById(R.id.edit_word)
val button = findViewById<Button>(R.id.button_save)
button.setOnClickListener {
val replyIntent = Intent()
if (TextUtils.isEmpty(editWordView.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val word = editWordView.text.toString()
replyIntent.putExtra(EXTRA_REPLY, word)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
companion object {
const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
}
}
A etapa final é conectar a IU ao banco de dados salvando novas palavras inseridas pelo usuário e exibindo o conteúdo atual do banco de dados de palavras na RecyclerView
.
Para exibir o conteúdo atual do banco de dados, adicione um observador para observar LiveData
no ViewModel
.
Sempre que os dados são modificados, o callback onChanged()
é invocado e chama o método setWords()
do adaptador para atualizar os dados em cache e a lista exibida.
Em MainActivity
, crie uma variável de membro para o ViewModel
:
private lateinit var wordViewModel: WordViewModel
Use ViewModelProvider
para associar a ViewModel
ao seu Activity
.
Quando a Activity
for iniciada pela primeira vez, a ViewModelProviders
criará a ViewModel
. Quando a atividade é destruída, por exemplo, por uma mudança de configuração, o ViewModel
persiste. Quando a atividade for recriada, o ViewModelProviders
retornará o ViewModel
já existente. Para mais informações, consulte ViewModel
.
Em onCreate()
, abaixo do bloco de código RecyclerView
, acesse um ViewModel
no ViewModelProvider
:
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
Também em onCreate()
, adicione um observador para a propriedade LiveData
allWords do WordViewModel
.
O método onChanged()
, padrão do nosso Lambda, é acionado quando os dados observados mudam e a atividade está em primeiro plano:
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
Queremos abrir a NewWordActivity
ao tocar no FAB e, quando voltarmos à MainActivity
, para inserir a nova palavra no banco de dados ou exibir um Toast
. Para fazer isso, vamos começar definindo um código de solicitação:
private val newWordActivityRequestCode = 1
Na MainActivity
, adicione o código onActivityResult()
para a NewWordActivity
.
Se a atividade retornar com RESULT_OK
, insira a palavra retornada no banco de dados chamando o método insert()
do WordViewModel
:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
Na MainActivity,
inicie NewWordActivity
quando o usuário tocar no FAB. No onCreate
da MainActivity
, localize o FAB e adicione um onClickListener
com este código:
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
O código finalizado ficará assim:
class MainActivity : AppCompatActivity() {
private const val newWordActivityRequestCode = 1
private lateinit var wordViewModel: WordViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
}
Agora, execute seu app. Quando você adicionar uma palavra ao banco de dados em NewWordActivity
, a IU será atualizada automaticamente.
Agora que temos um app em funcionamento, vamos recapitular o que foi criado. Veja a estrutura do app novamente:
Os componentes do app são os seguintes:
MainActivity
: exibe palavras em uma lista usando umaRecyclerView
e oWordListAdapter
. EmMainActivity
, há umObserver
que observa as palavras LiveData do banco de dados e é notificada quando elas mudam.NewWordActivity:
adiciona uma nova palavra à lista.WordViewModel
: fornece métodos para acessar a camada de dados e retorna LiveData para que a MainActivity possa configurar o relacionamento do observador.*LiveData<List<Word>>
: possibilita atualizações automáticas nos componentes da IU. NoMainActivity
, há umObserver
que observa as palavras LiveData do banco de dados e é notificada quando elas mudam.Repository:
gerencia uma ou mais fontes de dados. ORepository
expõe métodos para que o ViewModel interaja com o provedor de dados. Neste app, esse back-end é um banco de dados da Room.Room
: é um wrapper e implementa um banco de dados SQLite. A Room realiza muitas tarefas por você.- DAO: mapeia chamadas de método para consultas ao banco de dados, de modo que, quando o repositório chamar um método, como
getAlphabetizedWords()
, a Room poderá executarSELECT * from word_table ORDER BY word ASC
. Word
: é a classe de entidade que contém uma única palavra.
* Views
e Activities
(e Fragments
) só interagem com os dados usando o ViewModel
. Dessa forma, não importa de onde vêm os dados.
Fluxo de dados para atualizações automáticas da IU (IU reativa)
A atualização automática é possível porque estamos usando LiveData. Na MainActivity
, há um Observer
que observa o LiveData das palavras do banco de dados e é notificado quando elas mudam. Quando há uma mudança, o método onChange()
do observador é executado e atualiza mWords
no WordListAdapter
.
Os dados podem ser observados porque são LiveData
. E o que é observado é o LiveData<List<Word>>
que é retornado pela propriedade WordViewModel
a llWords
.
O WordViewModel
oculta tudo sobre o back-end da camada de IU. Ele fornece métodos para acessar a camada de dados e retorna LiveData
para que MainActivity
possa configurar a relação do observador. Views
e Activities
(e Fragments
) somente interagem com os dados usando o ViewModel
. Dessa forma, não importa de onde vêm os dados.
Nesse caso, eles são provenientes de um Repository
. O ViewModel
não precisa saber com o que o repositório interage. Ele só precisa saber como interagir com o Repository
, usando os métodos expostos pelo Repository
.
O repositório gerencia uma ou mais fontes de dados. No app WordListSample
, esse back-end é um banco de dados da Room. A Room é um wrapper e implementa um banco de dados SQLite. A Room realiza muitas tarefas por você. Por exemplo, a Room faz tudo o que você precisava fazer com uma classe SQLiteOpenHelper
.
O DAO mapeia chamadas de método para consultas do banco de dados, de modo que, quando o repositório chamar um método como getAllWords()
, a Room poderá executar SELECT * from word_table ORDER BY word ASC
.
Como o resultado retornado da consulta é observado em LiveData
, toda vez que os dados na Room mudam, o método onChanged()
da interface Observer
é executado e a IU é atualizada.
Opcional: fazer o download do código da solução
Consulte o código da solução deste codelab, caso ainda não tenha feito isso. Você pode analisar o repositório do GitHub (link em inglês) ou fazer o download do código aqui:
Descompacte o arquivo ZIP transferido por download. Isso descompactará uma pasta raiz, android-room-with-a-view-kotlin
, que contém o app completo.