El propósito de los componentes de la arquitectura es brindar orientación sobre la arquitectura de las apps, con bibliotecas para tareas comunes, como la administración del ciclo de vida y la persistencia de datos. Los componentes de la arquitectura te ayudan a estructurar la app de una manera sólida, que se puede probar y que se puede mantener con menos código estándar. Las bibliotecas de componentes de la arquitectura son parte de Android Jetpack.
Esta es la versión del codelab para Kotlin. Puedes encontrar la versión para el lenguaje de programación Java aquí.
Si a medida que avanzas con este codelab encuentras algún problema (errores de código, errores gramaticales, texto poco claro, etc.), infórmalo mediante el vínculo Informa un error que se encuentra en la esquina inferior izquierda del codelab.
Requisitos previos
Debes estar familiarizado con Kotlin, con los conceptos de diseño orientados a objetos y con los aspectos básicos de desarrollo de Android, en particular:
RecyclerView
y adaptadores.- Base de datos SQLite y lenguaje de consulta SQLite.
- Corrutinas básicas (si no estás familiarizado con las corrutinas, puedes revisar Cómo usar corrutinas de Kotlin en tu app para Android).
Además, es útil familiarizarse con los patrones arquitectónicos de software que separan datos de la interfaz de usuario, como MVP o MVC. En este codelab, se implementará la arquitectura que se define en la Guía de arquitectura de apps.
Este codelab está enfocado en componentes de la arquitectura de Android. El código y los conceptos no relevantes se proporcionan para que simplemente los copies y pegues.
Si no estás familiarizado con Kotlin, aquí te proporcionamos una versión de este codelab en el lenguaje de programación Java.
Actividades
En este codelab, aprenderás a diseñar y construir una app con los componentes de arquitectura Room, ViewModel y LiveData. También crearás una app que haga lo siguiente:
- Implementa nuestra arquitectura recomendada con los componentes de la arquitectura de Android.
- Funciona con una base de datos para obtener y guardar los datos, y prepropaga la base de datos con algunas palabras.
- Muestra todas las palabras de una
RecyclerView
enMainActivity
. - Abrir una segunda actividad cuando el usuario presiona el botón "+" Cuando el usuario ingresa una palabra, la agrega a la base de datos y a la lista.
La app es simple, pero lo suficientemente compleja para que puedas usarla como plantilla. Aquí tienes una vista previa:
Requisitos
- Android Studio 3.0 o posterior y conocimiento sobre cómo usarlo (asegúrate de que Android Studio esté actualizado, así como el SDK y Gradle)
- Un dispositivo o emulador de Android
Este codelab proporciona todo el código necesario para compilar la app completa.
Hay muchos pasos para usar los componentes de la arquitectura e implementar la arquitectura recomendada. Lo más importante es crear un modelo mental de lo que sucede, entender cómo las piezas se relacionan entre sí y cómo fluyen los datos. A medida que trabajes en este codelab, intenta comenzar a desarrollar la comprensión interna del código y no simplemente copiarlo y pegarlo.
¿Cuáles son los componentes recomendados de la arquitectura?
Para presentar la terminología, esta es una breve introducción a los componentes de la arquitectura y su funcionamiento en conjunto. Ten en cuenta que este codelab se enfoca en un subconjunto de los componentes; en particular, LiveData, ViewModel y Room. Cada componente se explica con más detalle a medida que lo usas.
Puedes ver una forma básica de la arquitectura en este diagrama:
Entidad: Es una clase anotada que describe una tabla de base de datos cuando se trabaja con Room.
Base de datos SQLite: Es el almacenamiento del dispositivo. La biblioteca de persistencias de Room crea y mantiene esta base de datos por ti.
DAO: Es el objeto de acceso a datos. Es un mapeo de búsquedas de SQL a las funciones Si usas un DAO, tú llamas a los métodos y Room se encarga del resto.
Base de datos de Room: Simplifica el trabajo de la base de datos y sirve como punto de acceso a la base de datos SQLite subyacente (oculta SQLiteOpenHelper)
). La base de datos de Room usa el DAO para enviar consultas a la base de datos SQLite.
Repositorio: Es una clase que creas que se usa principalmente para administrar varias fuentes de datos.
ViewModel: Actúa como un centro de comunicación entre el repositorio (datos) y la IU. La IU ya no tiene que preocuparse por el origen de los datos. Las instancias de ViewModel sobreviven a la recreación por actividad o fragmento.
LiveData: Una clase contenedora de datos observable. Siempre conserva o almacena en caché la última versión de los datos, y notifica a los observadores cuando los datos han cambiado. LiveData
está optimizado para los ciclos de vida. Los componentes de IU solo observan los datos relevantes y no detienen ni reanudan la observación. LiveData se ocupa automáticamente de todo esto, ya que está al tanto de los cambios de estado del ciclo de vida relevantes mientras lleva a cabo la observación.
Descripción general de la arquitectura de RoomWordSample
El diagrama siguiente muestra todas las partes de la app. Cada uno de los cuadros adjuntos (a excepción de la base de datos SQLite) representa una clase que crearás.
- Abre Android Studio y haz clic en Start a new Android Studio project.
- En la ventana Create New Project, selecciona Empty Activity y haz clic en Next.
- En la pantalla siguiente, asigna un nombre a la app RoomWordSample y haz clic en Finish.
A continuación, deberás agregar las bibliotecas de componentes a tus archivos de Gradle.
- En Android Studio, haz clic en la pestaña Projects y expande la carpeta de secuencias de comandos de Gradle.
Abre build.gradle
(Módulo: app).
- Para aplicar el complemento procesador de anotaciones
kapt
de Kotlin, agrégalo después de los otros complementos definidos en la parte superior de tu archivobuild.gradle
(Módulo: app).
apply plugin: 'kotlin-kapt'
- Agrega el bloque
packagingOptions
dentro del bloqueandroid
para excluir el módulo de funciones atómicas del paquete y evitar advertencias.
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
- Agrega el siguiente código al final del bloque
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"
- En tu archivo
build.gradle
(Project: RoomWordsSample), agrega los números de versión al final del archivo, como se indica en el código que aparece a continuación.
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}
Los datos de esta aplicación son palabras y necesitarás una tabla sencilla para contener esos valores:
Room te permite crear tablas a través de una entidad. Hagámoslo ahora.
- Crea un nuevo archivo de clase de Kotlin llamado
Word
que contenga la clase de datosWord
.
Esta clase describirá la entidad (que representa la tabla SQLite) para tus palabras. Cada propiedad de la clase representará una columna en la tabla. Finalmente, Room usará estas propiedades para construir la tabla y crear instancias de objetos a partir de filas en la base de datos.
Este es el código:
data class Word(val word: String)
A fin de que la clase Word
sea significativa para una base de datos de Room, debes anotarla. Las anotaciones identifican cómo cada parte de esta clase se relaciona con una entrada en la base de datos. Room usa esta información para generar código.
Si escribes las anotaciones por tu cuenta (en lugar de pegarlas), Android Studio importará las clases de anotación automáticamente.
- Actualiza tu clase
Word
con anotaciones como se muestra en este código:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
Veamos qué hacen estas anotaciones:
@Entity(tableName =
"word_table"
)
Cada clase@Entity
representa una tabla SQLite. Anota tu declaración de clase para indicar que es una entidad. Puedes especificar el nombre de la tabla si deseas que sea diferente al nombre de la clase. De esta forma, la tabla se llamará "word_table".@PrimaryKey
Todas las entidades necesitan una clave primaria. Para simplificar las cosas, cada palabra actúa como su propia clave primaria.@ColumnInfo(name =
"word"
)
Especifica el nombre de la columna en la tabla si deseas que sea diferente del nombre de la variable de miembro. De esta forma, la columna se llamará "word".- Cada propiedad que se almacena en la base de datos debe tener visibilidad pública, que es el valor predeterminado de Kotlin.
Puedes encontrar una lista completa de las anotaciones en la referencia del resumen del paquete de Room.
¿Qué es el DAO?
En el DAO (objeto de acceso a los datos), se especifican las consultas de SQL y se las asocia con llamadas de método. El compilador revisa el SQL y genera consultas a partir de anotaciones convenientes para consultas comunes, como @Insert
. Room usa el DAO con el objetivo de crear una API limpia para tu código.
El DAO debe ser una interfaz o una clase abstracta.
Todas las consultas deben ejecutarse en un subproceso separado de forma predeterminada.
Room tiene compatibilidad con corrutinas, lo que permite que tus búsquedas se anoten con el modificador suspend
y, luego, se las llame desde una corrutina o desde otra función de suspensión.
Cómo implementar el DAO
Escribamos un DAO que proporcione búsquedas para lo siguiente:
- Ordenar todas las palabras alfabéticamente
- Insertar una palabra
- Borrar todas las palabras
- Crea un archivo de clase de Kotlin nuevo llamado
WordDao
. - Copia y pega el siguiente código en
WordDao
y corrige las importaciones según sea necesario para compilarse.
@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()
}
Veamos cómo hacerlo paso a paso.
WordDao
es una interfaz. Los DAO deben ser interfaces o clases abstractas.- La anotación
@Dao
lo identifica como una clase de DAO para Room. suspend fun insert(word: Word)
: Declara una función de suspensión para insertar una palabra.- La anotación
@Insert
es una anotación de método DAO especial para la que no necesitas proporcionar un SQL. (También hay anotaciones@Delete
y@Update
para borrar y actualizar filas, pero no las utilizarás en esta app). onConflict = OnConflictStrategy.IGNORE
: La estrategia onConflict seleccionada ignorará una palabra nueva si es exactamente la misma que una que ya esté en la lista. Para obtener más información sobre las estrategias de conflicto disponibles, consulta la documentación.suspend fun deleteAll()
: Declara una función suspendida para borrar todas las palabras.- No hay ninguna anotación conveniente para borrar varias entidades, por lo que se anota con el
@Query
genérico. @Query
("DELETE FROM word_table")
:@Query
requiere que proporciones una consulta de SQL como un parámetro de string a la anotación, lo que permite realizar consultas de lectura complejas y otras operaciones.fun getAlphabetizedWords(): List<Word>
: Es el método para obtener todas las palabras y mostrar unaList
deWords
.@Query(
"SELECT * from word_table ORDER BY word ASC"
)
: Es una búsqueda que muestra una lista de palabras ordenadas de forma ascendente.
Por lo general, cuando cambian los datos, realizas alguna acción, como mostrar los datos actualizados en la IU. Esto significa que deben observar los datos para poder reaccionar cuando cambien.
Según cómo se almacenen los datos, esto puede ser complicado. La observación de cambios en los datos de varios componentes de la app puede crear rutas de dependencia explícitas y rígidas entre los componentes. Esto dificulta la prueba y la depuración, entre otras cosas.
LiveData
, una clase de biblioteca de ciclo de vida para la observación de datos, resuelve este problema. Usa un valor de muestra de tipo LiveData
en la descripción del método y Room generará todo el código necesario para actualizar LiveData
cuando se actualice la base de datos.
En WordDao
, cambia la firma del método getAlphabetizedWords()
para que el List<Word>
que se muestra esté unida a LiveData
.
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): LiveData<List<Word>>
Más adelante en este codelab, harás un seguimiento de los cambios en los datos mediante un Observer
en MainActivity
.
¿Qué es una base de datos Room?
- Room es una capa de base de datos sobre una base de datos SQLite.
- Room se ocupa de las tareas rutinarias de las que solías encargarte con
SQLiteOpenHelper
. - Room usa el DAO para enviar consultas a su base de datos.
- De manera predeterminada, para evitar un rendimiento deficiente en la IU, Room no permite enviar consultas en el subproceso principal. Cuando las búsquedas de Room muestran
LiveData
, las consultas se ejecutan automáticamente de forma asíncrona en un subproceso, en segundo plano. - Room proporciona comprobaciones de tiempo de compilación de las sentencias de SQLite.
Cómo implementar la base de datos de Room
La clase de tu base de datos de Room debe ser abstracta y extender RoomDatabase
. Por lo general, solo necesitas una instancia de una base de datos de Room para toda la app.
Creemos una ahora.
- Crea un archivo de clase de Kotlin llamado
WordRoomDatabase
y agrégale este código:
// 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
}
}
}
}
Veamos el código paso a paso:
- La clase de la base de datos de Room debe ser
abstract
y extenderRoomDatabase
. - Puedes anotar la clase para que sea una base de datos Room con
@Database
y usar los parámetros de anotación para declarar las entidades que pertenecen a la base de datos y establecer el número de versión. Cada entidad corresponde a una tabla que se creará en la base de datos. Las migraciones de la base de datos están fuera del alcance de este codelab, por lo que configuraremosexportSchema
como falso para evitar una advertencia de compilación. En una app real, deberías pensar en establecer un directorio para que Room exporte el esquema a fin de que puedas comprobar el esquema actual en tu sistema de control de versión. - La base de datos expone el DAO con un método "get" abstracto para cada @Dao.
- Definimos un singleton para evitar que se abran varias instancias de la base de datos al mismo tiempo,
WordRoomDatabase,
. getDatabase
muestra el singleton. Se creará la base de datos la primera vez que acceda a ella, mediante el compilador de bases de datos de Room para crear un objetoRoomDatabase
en el contexto de la aplicación a partir de la claseWordRoomDatabase
y se le asignará el nombre"word_database"
.
¿Qué es un repositorio?
Una clase de repositorio abstrae el acceso a múltiples fuentes de datos. El repositorio no forma parte de las bibliotecas de componentes de la arquitectura, pero es una práctica recomendada para la separación del código y su arquitectura. Una clase de repositorio proporciona una API limpia para el acceso de datos al resto de la aplicación.
¿Por qué usar un repositorio?
Un repositorio administra las consultas y te permite usar varios backends. En el ejemplo más común, el repositorio implementa la lógica para decidir si debe recuperar datos de una red o usar resultados almacenados en caché de una base de datos local.
Cómo implementar el repositorio
Crea un archivo de clase de Kotlin llamado WordRepository
y pega el siguiente código:
// 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)
}
}
Las ideas principales son las siguientes:
- El DAO se pasa al constructor del repositorio, en lugar de la base de datos completa. Esto se debe a que solo necesita acceso al DAO, ya que contiene todos los métodos de lectura y escritura de la base de datos. No es necesario exponer toda la base de datos en el repositorio.
- La lista de palabras es una propiedad pública. Se inicializa al obtener la lista de palabras
LiveData
de Room. Esto es posible debido a la forma en la que definimos el métodogetAlphabetizedWords
para que muestreLiveData
en el paso "Clase LiveData". Room ejecuta todas las consultas en un subproceso separado. Luego,LiveData
observado notificará al observador en el subproceso principal cuando los datos hayan cambiado. - El modificador
suspend
le indica al compilador que debe llamarse desde una corrutina o desde otra función de suspensión.
¿Qué es un ViewModel?
La función de ViewModel
es proporcionar datos a la IU y sobrevivir a los cambios de configuración. Un ViewModel
actúa como un centro de comunicación entre el repositorio y la IU. También puedes usar un ViewModel
para compartir datos entre fragmentos. ViewModel forma parte de la biblioteca de ciclo de vida.
Para ver una guía introductoria sobre este tema, consulta ViewModel Overview
o la entrada de blog "ViewModels: A Simple Example".
¿Por qué deberías usar un ViewModel?
Un ViewModel
contiene los datos de la IU de tu app de una manera optimizada para los ciclos de vida que sobrevive a los cambios de configuración. La separación entre los datos de IU de tu app y las clases Activity
y Fragment
te permite seguir mejor el principio de responsabilidad única: las actividades y los fragmentos son responsables de dibujar datos en la pantalla, mientras que tu ViewModel
puede encargarse de contener y procesar todos los datos necesarios para la IU.
En ViewModel
, usa LiveData
para datos modificables que la IU usará o mostrará. El uso de LiveData
ofrece varios beneficios:
- Puedes colocar un observador de los datos (en lugar de sondear los cambios) y solo actualizar la
la IU cuando los datos realmente cambien. - El repositorio y la IU están completamente separados por
ViewModel
. - No hay llamadas a la base de datos desde el
ViewModel
(todo esto se maneja en el repositorio), lo que hace que el código sea más fácil de probar.
viewModelScope
En Kotlin, todas las corrutinas se ejecutan dentro de un CoroutineScope
. Un alcance controla las corrutinas desde el principio con su trabajo. Cuando cancelas el trabajo de un alcance, se cancelan todas las corrutinas que se iniciaron en ese alcance.
La biblioteca lifecycle-viewmodel-ktx
de AndroidX agrega un viewModelScope
como una función de extensión de la clase ViewModel
, lo que te permite trabajar con alcances.
Si quieres obtener más información sobre cómo trabajar con corrutinas en el ViewModel, consulta el paso 5 del codelab Cómo usar corrutinas de Kotlin en tu app de Android o la entrada de blog Corrutinas simples en Android: viewModelScope.
Cómo implementar ViewModel
Crea un archivo de clase de Kotlin para WordViewModel
y agrégale este código:
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)
}
}
Lo que hicimos hasta aquí:
- Creamos una clase llamada
WordViewModel
que obtiene elApplication
como parámetro y extiendeAndroidViewModel
. - Se agregó una variable de miembro privado para mantener una referencia al repositorio.
- Agregamos una variable de miembro
LiveData
pública para almacenar en caché la lista de palabras. - Se creó un bloque
init
que obtiene una referencia aWordDao
desdeWordRoomDatabase
. - En el bloque
init
, se construye elWordRepository
en función de laWordRoomDatabase
. - En el bloque
init
, inicializaste el LiveData deallWords
con el repositorio. - Creamos un método
insert()
de wrapper que llama al métodoinsert()
del repositorio. De esta manera, la implementación deinsert()
se encapsula desde la IU, No queremos insertar inserción para bloquear el subproceso principal, por lo que estamos lanzando una corrutina nueva y llamando a la inserción del repositorio, que es una función de suspensión. Como se mencionó antes, ViewModels tiene un alcance de corrutinas basado en los ciclos de vida, llamadoviewModelScope
, que usamos aquí.
A continuación, debes agregar el diseño XML para la lista y los elementos.
En este codelab, se asume que estás familiarizado con la creación de diseños en XML. Por ello, solo te proporcionamos el código.
Configura el AppTheme
como superior a Theme.MaterialComponents.Light.DarkActionBar
para crear tu material de temas de la aplicación. Agrega un estilo para los elementos de lista en 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>
Agrega un diseño 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>
En layout/activity_main.xml
, reemplaza TextView
por RecyclerView
y agrega un botón de acción flotante (BAF). Ahora, tu diseño debería verse así:
<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>
La apariencia de tu BAF debería corresponderse con la acción disponible, por lo que debemos reemplazar el ícono por el símbolo &'
En primer lugar, debemos agregar un elemento vectorial nuevo:
- Selecciona File > New > Vector Asset.
- Haz clic en el ícono de robot de Android en el campo Clip Art:.
- Busca &addt; y selecciona el elemento '+' Haz clic en OK.
- Luego, haz clic en Siguiente.
- Confirma la ruta de acceso del ícono como
main > drawable
y haz clic en Finish para agregar el elemento. - Aún en
layout/activity_main.xml
, actualiza el BAF para incluir el nuevo elemento de diseño:
<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"/>
Los datos se mostrarán en RecyclerView
, que es un poco más agradable que solo arrojarlos en un TextView
. En este codelab, se supone que sabes cómo funcionan RecyclerView
, RecyclerView.LayoutManager
, RecyclerView.ViewHolder
y RecyclerView.Adapter
.
Ten en cuenta que la variable words
del adaptador almacena en caché los datos. En la próxima tarea, agregará el código que actualiza los datos automáticamente.
Crea un archivo de clase de Kotlin para WordListAdapter
que extienda RecyclerView.Adapter
. Este es el 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
}
Agrega RecyclerView
al método onCreate()
de MainActivity
.
En el método onCreate()
, después de setContentView
:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
Ejecuta tu app para asegurarte de que todo funcione correctamente. No hay elementos porque aún no has agregado los datos.
No hay datos en la base de datos. Agregarás datos de dos maneras: algunos cuando se abra la base de datos, y un Activity
para agregar palabras.
Crea una RoomDatabase.Callback
y anula onOpen()
para borrar todo el contenido y volver a propagar la base de datos cada vez que se inicia la app. Como no puedes realizar operaciones de la base de datos de Room en el subproceso de IU, onOpen()
inicia una corrutina en el despachador de IO.
Para iniciar una corrutina, necesitamos un CoroutineScope
. Actualiza el método getDatabase
de la clase WordRoomDatabase
a fin de obtener también un alcance de corrutina como parámetro:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
Actualiza el inicializador de recuperación de la base de datos en el bloque init
de WordViewModel
para pasar también el permiso:
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
En el WordRoomDatabase
, creamos una implementación personalizada de RoomDatabase.Callback()
, que también obtiene un objeto CoroutineScope
como parámetro del constructor. Luego, anulamos el método onOpen
para propagar la base de datos.
Este es el código para crear la devolución de llamada dentro de la clase 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 último, agrega la devolución de llamada a la secuencia de compilación de la base de datos justo antes de llamar a .build()
en el Room.databaseBuilder()
:
.addCallback(WordDatabaseCallback(scope))
Así se verá el código final:
@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
}
}
}
}
Agrega estos recursos de string a 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>
Agrega este recurso de color a value/colors.xml
:
<color name="buttonLabel">#FFFFFF</color>
Crea un archivo de recursos de dimensión nuevo:
- Haz clic en el módulo de la app en la ventana Project.
- Selecciona File > New > Android Resource File.
- En los calificadores disponibles, selecciona Dimension.
- Establece el nombre del archivo: dimens.
Agrega estos recursos de dimensión a values/dimens.xml
:
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
Crea un Activity
de Android vacío nuevo con la plantilla Empty Activity:
- Selecciona File > New > Activity > Empty Activity.
- Ingresa
NewWordActivity
en el nombre de la actividad. - Verifica que se haya agregado la nueva actividad al manifiesto de Android.
<activity android:name=".NewWordActivity"></activity>
Actualiza el archivo activity_new_word.xml
de la carpeta de diseño con el siguiente 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>
Actualiza el código de la actividad:
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"
}
}
El paso final es conectar la IU a la base de datos. Para ello, guarda las palabras nuevas que el usuario ingrese y muestra el contenido actual de la base de datos de palabra en el RecyclerView
.
Para mostrar el contenido actual de la base de datos, agrega un observador que observe LiveData
en el ViewModel
.
Cuando cambian los datos, se invoca la devolución de llamada onChanged()
, que llama al método setWords()
del adaptador para actualizar los datos almacenados en caché del adaptador y actualizar la lista que se muestra.
En MainActivity
, crea una variable de miembro para ViewModel
:
private lateinit var wordViewModel: WordViewModel
Usa ViewModelProvider
para asociar tu ViewModel
con tu Activity
.
Cuando comience la Activity
, el ViewModelProviders
creará la ViewModel
. Cuando se destruye la actividad, por ejemplo, a través de un cambio de configuración, el elemento ViewModel
persiste. Cuando se vuelve a crear la actividad, el objeto ViewModelProviders
muestra el ViewModel
existente. Para obtener más información, consulta ViewModel
.
En onCreate()
, debajo del bloque de código RecyclerView
, obtén una ViewModel
de la ViewModelProvider
:
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
También en onCreate()
, agrega un observador para la propiedad LiveData
de allWords desde el WordViewModel
.
El método onChanged()
(el método predeterminado para Lambda) se activa cuando se modifican los datos observados y la actividad está en primer plano:
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
Queremos que, cuando se presiona el BAF, se abra la NewWordActivity
y, una vez que regresamos a la MainActivity
, insertar la nueva palabra en la base de datos o mostrar un Toast
. Para lograrlo, comencemos por definir un código de solicitud:
private val newWordActivityRequestCode = 1
En MainActivity
, agrega el código onActivityResult()
para la NewWordActivity
.
Si la actividad muestra RESULT_OK
, inserta la palabra que se mostró en la base de datos mediante una llamada al método insert()
de 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()
}
}
En MainActivity,
, inicia NewWordActivity
cuando el usuario presione el BAF. En la onCreate
MainActivity
, busca el BAF y agrega un onClickListener
con este código:
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
El código finalizado debería verse así:
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()
}
}
}
Ahora ejecuta tu app. Cuando agregues una palabra a la base de datos en NewWordActivity
, la IU se actualizará de forma automática.
Ahora que tienes una app funcional, repasemos lo que compilaste. A continuación, se muestra la estructura de la app una vez más:
Los componentes de la aplicación son los siguientes:
MainActivity
: Muestra palabras en una lista mediante unRecyclerView
yWordListAdapter
. EnMainActivity
, hay unObserver
que observa las palabras LiveData de la base de datos y recibe una notificación cuando cambian.NewWordActivity:
agrega una palabra nueva a la lista.WordViewModel
: Proporciona métodos para acceder a la capa de datos, y muestra LiveData para que MainActivity pueda configurar la relación del observador.*LiveData<List<Word>>
: Permite que las actualizaciones sean automáticas para los componentes de la IU. En elMainActivity
, hay unObserver
que observa las palabras LiveData de la base de datos y recibe una notificación cuando cambian.Repository:
administra una o más fuentes de datos. ElRepository
muestra los métodos para que ViewModel interactúe con el proveedor de datos subyacente. En esta aplicación, ese backend es una base de datos de Room.Room
: es un wrapper e implementa una base de datos SQLite. Room hace por ti mucho del trabajo que solías hacer.- DAO: Asigna llamadas de método a consultas de la base de datos para que, cuando el repositorio llame a un método como
getAlphabetizedWords()
, Room pueda ejecutarSELECT * from word_table ORDER BY word ASC
. Word
: es la clase de entidad que contiene una sola palabra.
* Views
y Activities
(y Fragments
) solo interactúan con los datos mediante ViewModel
. Por lo tanto, es irrelevante de dónde provengan los datos.
Flujo de datos para las actualizaciones automáticas de la IU (IU reactiva)
La actualización automática es posible porque estamos usando LiveData. En el MainActivity
, hay un Observer
que observa las palabras LiveData de la base de datos y recibe una notificación cuando cambian. Cuando hay un cambio, el método onChange()
del observador se ejecuta y se actualiza mWords
en el WordListAdapter
.
Los datos pueden observarse porque son LiveData
Y lo que se observa es la LiveData<List<Word>>
que muestra la propiedad WordViewModel
allWords
.
El WordViewModel
oculta toda la información del backend de la capa de IU. Proporciona métodos para acceder a la capa de datos y muestra LiveData
para que MainActivity
pueda configurar la relación del observador. Views
y Activities
(y Fragments
) solo interactúan con los datos mediante ViewModel
. Por lo tanto, es irrelevante de dónde provengan los datos.
En este caso, los datos provienen de un Repository
. El ViewModel
no necesita saber con qué interactúa el repositorio. Solo necesita saber cómo interactuar con el Repository
, que se realiza con los métodos que muestra el Repository
.
El repositorio administra una o más fuentes de datos. En la app WordListSample
, ese backend es una base de datos de Room. Room es un wrapper que implementa una base de datos SQLite. Room hace por ti mucho del trabajo que solías hacer. Por ejemplo, Room hace todo lo que solías hacer con una clase SQLiteOpenHelper
.
El DAO asigna llamadas de método a las consultas de la base de datos para que, cuando el repositorio llame a un método como getAllWords()
, Room pueda ejecutar SELECT * from word_table ORDER BY word ASC
.
Como el resultado que muestra la consulta es LiveData
observado, cada vez que se modifican los datos de Room, se ejecuta el método onChanged()
de la interfaz Observer
y se actualiza la IU.
[Opcional] Descarga el código de la solución
Si aún no lo hiciste, puedes ver el código de la solución para el codelab. Puedes mirar el repositorio de GitHub o descargar el código aquí:
Descomprime el archivo zip descargado. Esto descomprimirá una carpeta raíz, android-room-with-a-view-kotlin
, que contiene la app completa.