Introducción a Room y Flow

1. Antes de comenzar

En el codelab anterior, aprendiste sobre los aspectos básicos de las bases de datos relacionales y sobre cómo leer y escribir datos con los comandos de SQL SELECT, INSERT, UPDATE y DELETE. Aprender a trabajar con bases de datos relacionales es una habilidad fundamental que usarás en toda tu carrera de programación. Saber cómo funcionan las bases de datos relacionales también es esencial a la hora de implementar la persistencia de datos en una aplicación para Android, que es lo que harás en esta lección.

Una manera sencilla de usar una base de datos en una app para Android es con una biblioteca llamada Room, que es una biblioteca ORM ("Object Relational Mapping", o asignación relacional de objetos). Como su nombre lo indica, asigna las tablas de una base de datos relacional a objetos que se pueden usar en el código de Kotlin. En esta lección, solo te centrarás en leer datos. Con una base de datos prepropagada, cargarás datos de una tabla de horarios de llegada de autobuses y los presentarás en una RecyclerView.

70c597851eba9518.png

Durante el proceso, aprenderás sobre los aspectos básicos del uso de Room, incluida la clase de base de datos, el DAO, las entidades y los modelos de vista. También obtendrás la clase ListAdapter, otra forma de presentar datos en una RecyclerView, y un flujo, una función de lenguaje Kotlin similar a LiveData que permitirá que tu IU responda a los cambios en la base de datos.

Requisitos previos

  • Estar familiarizado con la programación orientada a objetos y el uso de clases, objetos y herencia en Kotlin
  • Tener conocimiento básico de bases de datos relacionales y SQL, tal como se explica en el codelab de aspectos básicos de SQL
  • Tener experiencia con corrutinas de Kotlin

Qué aprenderás

Al final de esta lección, deberías poder hacer lo siguiente:

  • Representar tablas de bases de datos como objetos de Kotlin (entidades)
  • Definir la clase de base de datos para usar Room en la app y prepropagar una base de datos desde un archivo
  • Definir la clase DAO y usar consultas de SQL para acceder a la base de datos desde el código de Kotlin
  • Definir un modelo de vista para permitir que la IU interactúe con el DAO
  • Usar ListAdapter con una vista de reciclador
  • Conceptos básicos del flujo de Kotlin y cómo usarlo para que la IU responda a los cambios en los datos subyacentes

Qué compilarás

  • Leerás datos de una base de datos prepropagada con Room y los presentarás en una vista de reciclador en una app simple de horarios de autobuses.

2. Cómo comenzar

La app con la que trabajarás en este laboratorio de código se llama "Bus Schedule". Presenta una lista de paradas de autobuses y horarios de llegada, del más temprano al más tarde.

70c597851eba9518.png

Si presionas una fila de la primera pantalla, se convertirá en una nueva pantalla que mostrará solo los próximos horarios de llegada para la parada de autobús seleccionada.

f477c0942746e584.png

Los datos de paradas de autobús provienen de una base de datos que se incluye con la app. Sin embargo, en su estado actual, no se mostrará nada cuando la app se ejecute por primera vez. Tu trabajo es integrar Room para que la app muestre la base de datos prepropagada de los horarios de llegada.

  1. Navega a la página de repositorio de GitHub del proyecto.
  2. Verifica que el nombre de la rama coincida con el especificado en el codelab. Por ejemplo, en la siguiente captura de pantalla, el nombre de la rama es main.

1e4c0d2c081a8fd2.png

  1. En la página de GitHub de este proyecto, haz clic en el botón Code, el cual abre una ventana emergente.

1debcf330fd04c7b.png

  1. En la ventana emergente, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.

Abre el proyecto en Android Studio

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open.

d8e9dbdeafe9038a.png

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > Open.

8d1fda7396afe8e5.png

  1. En el navegador de archivos, ve hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run 8de56cba7583251f.png para compilar y ejecutar la app. Asegúrate de que funcione como se espera.

3. Agrega una dependencia de Room

Al igual que con cualquier otra biblioteca, primero debes agregar las dependencias necesarias para poder usar Room en la app de Bus Schedule. Para ello, solo necesitarás hacer dos cambios pequeños, uno en cada archivo de Gradle.

  1. En el archivo build.gradle de nivel de proyecto, define room_version en el bloque ext.
ext {
   kotlin_version = "1.6.20"
   nav_version = "2.4.1"
   room_version = '2.4.2'
}
  1. En el archivo build.gradle de nivel de app, al final de la lista de dependencias, agrega las siguientes dependencias.
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"
  1. Sincroniza los cambios y compila el proyecto para verificar que se hayan agregado correctamente las dependencias.

En las siguientes páginas, conocerás los componentes necesarios para integrar Room en una app: modelos, DAO, modelos de vista y la clase de base de datos.

4. Crea una entidad

Cuando aprendiste sobre bases de datos relacionales en el codelab anterior, viste cómo los datos se organizaban en tablas que constaban de varias columnas, cada una de las cuales representa una propiedad específica de un tipo de datos específico. Así como las clases en Kotlin proporcionan una plantilla para cada objeto, una tabla de una base de datos proporciona una plantilla para cada elemento o fila de esa tabla. No debería sorprenderte que se pueda usar una clase de Kotlin para representar cada tabla de una base de datos.

Cuando se trabaja con Room, cada tabla se representa con una clase. En una biblioteca ORM, como Room, estas suelen llamarse clases de modelos o entidades.

La base de datos para la app de Bus Schedule solo consiste en una tabla, un cronograma que incluye información básica sobre la llegada de un autobús.

  • id: Es un número entero que proporciona un identificador único que sirve como clave primaria.
  • stop_name: Es una string.
  • arrival_time: Es un valor entero.

Ten en cuenta que los tipos de SQL que se usan en la base de datos son, en realidad, INTEGER para Int y TEXT para String. Sin embargo, cuando trabajes con Room, solo deberás preocuparte por los tipos de Kotlin cuando definas tus clases de modelos. La asignación de los tipos de datos de tu clase de modelos a los que se usan en la base de datos se controla automáticamente.

Si un proyecto tiene muchos archivos, deberías organizar los archivos en diferentes paquetes a fin de tener un mejor control de acceso para cada clase y facilitar la ubicación de las clases relacionadas. A fin de crear una entidad para la tabla "schedule", en el paquete com.example.busschedule, agrega un paquete nuevo llamado database. Dentro de ese paquete, agrega uno nuevo llamado schedule para tu entidad. Luego, en el paquete database.schedule, crea un archivo nuevo llamado Schedule.kt y define una clase de datos llamada Schedule.

data class Schedule(
)

Como se explicó en la lección Conceptos básicos de SQL, las tablas de datos deben tener una clave primaria para identificar de manera única cada fila. La primera propiedad que agregarás a la clase Schedule es un número entero que representa un ID único. Agrega una propiedad nueva y márcala con la anotación @PrimaryKey. Esto le indica a Room que debe tratar esta propiedad como la clave primaria cuando se insertan filas nuevas.

@PrimaryKey val id: Int

Agrega una columna para el nombre de la parada de autobús. La columna debe ser del tipo String. En las columnas nuevas, deberás agregar una anotación @ColumnInfo para especificar su nombre. Por lo general, los nombres de las columnas de SQL tienen palabras separadas por un guion bajo, en comparación con el valor lowerCamelCase que usan las propiedades de Kotlin. En esta columna, no queremos que el valor sea nulo, por lo que debes marcarlo con la anotación @NonNull.

@NonNull @ColumnInfo(name = "stop_name") val stopName: String,

Los tiempos de llegada se representan en la base de datos con números enteros. Esta es una marca de tiempo Unix que se puede convertir en una fecha. Mientras que diferentes versiones de SQL ofrecen formas de convertir fechas, para tus propósitos, te quedarás con las funciones de formato de fecha de Kotlin. Agrega la siguiente columna @NonNull a la clase de modelos.

@NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int

Por último, para que Room reconozca esta clase como algo que se puede usar para definir tablas de base de datos, debes agregar una anotación a la clase misma. Agrega @Entity en una línea aparte antes del nombre de la clase.

De forma predeterminada, Room usa el nombre de la clase como el nombre de la tabla de la base de datos. Por lo tanto, el nombre de la tabla definido en este momento por la clase sería Schedule. De manera opcional, también puedes especificar @Entity(tableName="schedule"), pero, como las búsquedas de Room no distinguen mayúsculas de minúsculas, aquí puedes omitir explícitamente la definición de un nombre de tabla en minúsculas.

Ahora la clase correspondiente a la entidad de programación debería verse de la siguiente manera.

@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. Define el DAO

La siguiente clase que debes agregar para integrar Room es el DAO, que significa "Data Access Object" (objeto de acceso a datos) y es una clase de Kotlin que proporciona acceso a los datos. Específicamente, el DAO es donde deberías incluir funciones para leer y manipular datos. Llamar a una función en el DAO es el equivalente a ejecutar un comando de SQL en la base de datos. De hecho, las funciones DAO como las que definirás en esta app suelen especificar un comando de SQL para que puedas especificar exactamente lo que quieres que haga la función. Lo que aprendiste sobre SQL en el codelab anterior te resultará útil para definir el DAO.

  1. Agrega una clase de DAO para la entidad Schedule. En el paquete database.schedule, crea un archivo nuevo llamado ScheduleDao.kt y define una interfaz llamada ScheduleDao. Al igual que con la clase Schedule, debes agregar una anotación, esta vez @Dao, para que la interfaz se pueda usar con Room.
@Dao
interface ScheduleDao {
}
  1. La app tiene dos pantallas y cada una necesitará una consulta diferente. En la primera pantalla, se muestran todas las paradas de autobús en orden ascendente según la hora de llegada. En este caso práctico, la consulta solo necesita obtener todas las columnas y, también, incluir una cláusula ORDER BY adecuada. La consulta se especifica como una string que se pasa a una anotación @Query. Define una función getAll() que muestre una lista de objetos Schedule, incluida la anotación @Query, como se muestra.
@Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
fun getAll(): List<Schedule>
  1. Para la segunda consulta, también debes seleccionar todas las columnas de la tabla de horarios. Sin embargo, solo quieres resultados que coincidan con el nombre de la parada seleccionada, por lo que debes agregar una cláusula WHERE. Puedes hacer referencia a los valores de Kotlin de la consulta mediante dos puntos (:) (p. ej., :stopName del parámetro de función). Al igual que antes, los resultados se ordenan en forma ascendente por hora de llegada. Define una función getByStopName() que tome un parámetro String llamado stopName y muestre un List de objetos Schedule, con una anotación @Query como se muestra.
@Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC")
fun getByStopName(stopName: String): List<Schedule>

6. Define el ViewModel

Ahora que configuraste el DAO, tienes técnicamente todo lo que necesitas para comenzar a acceder a la base de datos desde tus fragmentos. Sin embargo, aunque esto es en teoría, por lo general, no se considera una práctica recomendada. Esto se debe a que, en las apps más complejas, es probable que tengas varias pantallas que accedan a solo una parte específica de los datos. Si bien ScheduleDao es relativamente simple, es fácil ver cómo se puede solucionar este problema cuando se trabaja con dos o más pantallas diferentes. Por ejemplo, un DAO puede verse así:

@Dao
interface ScheduleDao {

    @Query(...)
    getForScreenOne() ...

    @Query(...)
    getForScreenTwo() ...

    @Query(...)
    getForScreenThree()

}

Si bien el código de la Pantalla 1 puede acceder a getForScreenOne(), no hay una buena razón para acceder a otros métodos. En su lugar, se recomienda separar la parte del DAO que expones a la vista en una clase separada llamada modelo de vista. Este es un patrón de arquitectura común en apps para dispositivos móviles. El uso de un modelo de vista ayuda a aplicar una separación clara entre el código de la IU de tu app y su modelo de datos. También te permite probar cada parte de tu código de manera independiente, un tema que explorarás más adelante mientras continúes el proceso de desarrollador de Android.

ee2524be13171538.png

Si usas un modelo de vista, puedes aprovechar la clase ViewModel. Se usa la clase ViewModel para almacenar datos relacionados con la IU de una app, y también está optimizada para los ciclos de vida, lo que significa que responde a eventos de ciclo de vida de la misma manera que una actividad o un fragmento. Si los eventos del ciclo de vida, como la rotación de la pantalla, hacen que una actividad o un fragmento se destruya y se vuelva a crear, no hará falta que se vuelva a crear el ViewModel asociado. Como no se puede acceder directamente a una clase DAO, se recomienda usar la subclase ViewModel para separar la responsabilidad de cargar datos de tu actividad o fragmento.

  1. Para crear una clase de modelo de vista, crea un archivo nuevo llamado BuusScheduleViewModel.kt en un paquete nuevo llamado viewmodels. Define una clase para el modelo de vista. Debes usar un solo parámetro del tipo ScheduleDao.
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
  1. Dado que este modelo de vista se usará con ambas pantallas, deberás agregar un método para obtener el horario completo, así como un horario filtrado por nombre de parada. Para ello, llama a los métodos correspondientes de ScheduleDao.
fun fullSchedule(): List<Schedule> = scheduleDao.getAll()

fun scheduleForStopName(name: String): List<Schedule> = scheduleDao.getByStopName(name)

Aunque hayas terminado de definir el modelo de vista, no puedes simplemente crear una instancia de BusScheduleViewModel de forma directa y esperar que todo funcione sin más. Como la clase ViewModel que necesita BusScheduleViewModel debe estar optimizada para el ciclo de vida, un objeto que pueda responder a eventos de ciclo de vida debe ser el creador de la instancia. Si creas una instancia de forma directa en uno de los fragmentos, el objeto de fragmento deberá controlar todo, incluso toda la gestión de la memoria, y esto está fuera del alcance del código de tu app. En su lugar, puedes crear una clase, denominada fábrica, que creará instancias de objetos de modelo de vista.

  1. Para crear una fábrica, debajo de la clase de modelo de vista, crea una nueva clase BusScheduleViewModelFactory que se herede de ViewModelProvider.Factory.
class BusScheduleViewModelFactory(
   private val scheduleDao: ScheduleDao
) : ViewModelProvider.Factory {
}
  1. Solo necesitas un poco de código estándar para crear una instancia correcta de un modelo de vista. En lugar de inicializar la clase directamente, anularás un método llamado create(), que muestra una BusScheduleViewModelFactory con algunas verificaciones de errores. Implementa create() dentro de la clase BusScheduleViewModelFactory de la siguiente manera.
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")
   }

Ahora puedes crear una instancia de un objeto BusScheduleViewModelFactory con BusScheduleViewModelFactory.create(), de modo que tu modelo de vista pueda reconocer el ciclo de vida sin que tu fragmento deba procesarlo directamente.

7. Crea una clase de base de datos y prepropaga la base de datos

Ahora que definiste los modelos, el DAO y un modelo de vista para fragmentos a fin de acceder al DAO, debes indicarle a Room qué hacer con todas esas clases. Allí es donde entra en juego la clase AppDatabase. Una app para Android que usa Room, como la tuya, crea subclases de la clase RoomDatabase y tiene algunas responsabilidades clave. En tu app, AppDatabase necesita lo siguiente:

  1. Especificar qué entidades se definen en la base de datos
  2. Proporcionar acceso a una sola instancia de cada clase de DAO
  3. Realizar cualquier configuración adicional, como completar previamente la base de datos

Si te preguntas por qué Room no puede encontrar todas las entidades y los objetos DAO por ti, es posible que tu app tenga varias bases de datos o que la biblioteca no pueda determinar tu intención. La clase AppDatabase te brinda control total de tus modelos, clases DAO y cualquier configuración de base de datos que desees realizar.

  1. Para agregar una clase AppDatabase, en el paquete database, crea un archivo nuevo llamado AppDatabase.kt y define una nueva clase abstracta AppDatabase que se herede de RoomDatabase.
abstract class AppDatabase: RoomDatabase() {
}
  1. La clase de base de datos permite que otras clases accedan fácilmente a las clases DAO. Agrega una función abstracta que muestre un ScheduleDao.
abstract fun scheduleDao(): ScheduleDao
  1. Cuando usas una clase AppDatabase, debes asegurarte de que solo exista una instancia de la base de datos para evitar condiciones de carrera u otros problemas posibles. La instancia se almacena en el objeto complementario, y también necesitarás un método que muestre la instancia existente o cree la base de datos por primera vez. Esto se define en el objeto complementario. Agrega el siguiente elemento companion object justo debajo de la función scheduleDao().
companion object {
}

En companion object, agrega una propiedad llamada INSTANCE de tipo AppDatabase. Este valor se configura inicialmente como null, por lo que el tipo se marca con un ?. Esto también se marca con una anotación @Volatile. Si bien los detalles sobre cuándo usar una propiedad volátil son un poco avanzados para esta lección, te recomendamos usarla para tu instancia de AppDatabase a fin de evitar posibles errores.

@Volatile
private var INSTANCE: AppDatabase? = null

Debajo de la propiedad INSTANCE, define una función para mostrar la instancia 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
    }
}

En la implementación de getDatabase(), debes usar el operador Elvis para mostrar la instancia existente de la base de datos (si ya existe) o crear la base de datos por primera vez si es necesario. En esta app, debido a que los datos están prepropagados, también debes llamar a createFromAsset() para cargar los datos existentes. El archivo bus_schedule.db se encuentra en el paquete assets.database de tu proyecto.

  1. Al igual que las clases de modelo y DAO, la clase de base de datos requiere una anotación que proporcione información específica. Todos los tipos de entidades (accedes al tipo con ClassName::class) se enumeran en un array. La base de datos también recibe un número de versión, que establecerás en 1. Agrega la anotación @Database de la siguiente manera.
@Database(entities = arrayOf(Schedule::class), version = 1)

Ahora que creaste tu clase AppDatabase, solo queda un paso más para que se la pueda usar. Deberás proporcionar una subclase personalizada de la clase Application y crear una propiedad lazy que contenga el resultado de getDatabase().

  1. En el paquete com.example.busschedule, agrega un archivo nuevo llamado BusScheduleApplication.kt y crea una clase BusScheduleApplication que se herede de Application.
class BusScheduleApplication : Application() {
}
  1. Agrega una propiedad de base de datos de tipo AppDatabase. La propiedad debe ser diferida y mostrar el resultado de llamar a getDatabase() en tu clase AppDatabase.
class BusScheduleApplication : Application() {
   val database: AppDatabase by lazy { AppDatabase.getDatabase(this) }
  1. Por último, para asegurarte de que se use la clase BusScheduleApplication (en lugar de la clase base predeterminada Application), debes hacer un pequeño cambio en el manifiesto. En AndroidMainifest.xml, establece la propiedad android:name en com.example.busschedule.BusScheduleApplication.
<application
    android:name="com.example.busschedule.BusScheduleApplication"
    ...

De esta forma, configurarás el modelo de la app. Ya está todo listo para que comiences a usar los datos de Room en tu IU. En las próximas páginas, crearás un ListAdapter para la RecyclerView de tu app a fin de presentar los datos de los horarios de autobús y responder a los cambios de datos de forma dinámica.

8. Crea el ListAdapter

Es momento de realizar ese trabajo arduo y conectar el modelo a la vista. Anteriormente, cuando se usaba una RecyclerView, se utilizaba un RecyclerView.Adapter para presentar una lista estática de datos. Si bien esto funcionará para una app como Bus Schedule, una situación común al trabajar con bases de datos es controlar los cambios de datos en tiempo real. Incluso si solo cambia el contenido de un elemento, se actualiza toda la vista de reciclador. Eso no será suficiente para la mayoría de las apps que usen la persistencia.

Una alternativa para una lista que cambia dinámicamente es ListAdapter. ListAdapter usa AsyncListDiffer para determinar las diferencias entre una lista de datos antigua y una nueva. Luego, la vista de reciclador solo se actualiza en función de las diferencias entre las dos listas. Como resultado, tu vista de reciclador es más eficaz cuando se manejan datos que se actualizan con frecuencia, como lo harás en una aplicación de base de datos.

f59cc2fd4d72c551.png

Como la IU es idéntica para ambas pantallas, solo deberás crear un único ListAdapter que se pueda usar con ambas.

  1. Crea un nuevo archivo BusStopAdapter.kt y una clase BusStopAdapter como se muestra. La clase extiende un ListAdapter genérico que toma una lista de objetos Schedule y una clase BusStopViewHolder para la IU. Para BusStopViewHolder, también debes pasar un tipo DiffCallback que definirás pronto. La clase BusStopAdapter también toma un parámetro: onItemClicked(). Esta función se utilizará para controlar la navegación cuando se seleccione un elemento en la primera pantalla, pero en la segunda pantalla solo pasarás una función vacía.
class BusStopAdapter(private val onItemClicked: (Schedule) -> Unit) : ListAdapter<Schedule, BusStopAdapter.BusStopViewHolder>(DiffCallback) {
}
  1. Al igual que con un adaptador de vista de reciclador, necesitas un contenedor de vistas para poder acceder a las vistas creadas a partir de tu archivo de diseño. Ya se creó el diseño de las celdas. Simplemente crea una clase BusStopViewHolder como se muestra y, luego, implementa la función bind() para configurar el texto de stopNameTextView con el nombre de la parada y el texto de arrivalTimeTextView, que indica la fecha con formato.
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)
        )
    }
}
  1. Anula e implementa onCreateViewHolder(), y aumenta el diseño y configura el onClickListener() para llamar al onItemClicked() del elemento en la posición actual.
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
}
  1. Anula e implementa onBindViewHolder() para vincular la vista en la posición especificada.
override fun onBindViewHolder(holder: BusStopViewHolder, position: Int) {
   holder.bind(getItem(position))
}
  1. ¿Recuerdas la clase DiffCallback que especificaste para ListAdapter? Este es solo un objeto que ayuda a ListAdapter a determinar qué elementos de las listas nuevas y anteriores son diferentes cuando se actualiza la lista. Existen dos métodos: areItemsTheSame() verifica si el objeto (o la fila de la base de datos, en tu caso) es el mismo que verifica el ID. areContentsTheSame() verifica si todas las propiedades, no solo el ID, son iguales. Estos métodos permiten que ListAdapter determine qué elementos se insertaron, actualizaron y borraron para que la IU pueda actualizarse según corresponda.

Agrega un objeto complementario y, luego, implementa DiffCallback como se muestra.

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
       }
   }
}

Eso es todo lo que se necesita para configurar el adaptador. Lo usarás en ambas pantallas de la app.

  1. Primero, en FullScheduleFragment.kt, debes obtener una referencia al modelo de vista.
private val viewModel: BusScheduleViewModel by activityViewModels {
   BusScheduleViewModelFactory(
       (activity?.application as BusScheduleApplication).database.scheduleDao()
   )
}
  1. Luego, en onViewCreated(), agrega el siguiente código para configurar la vista de reciclador y asignar su administrador de diseño.
recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(requireContext())
  1. Después de eso, asigna la propiedad del adaptador. La acción pasada usará el stopName para navegar por la siguiente pantalla seleccionada a fin de filtrar la lista de paradas de autobuses.
val busStopAdapter = BusStopAdapter({
   val action = FullScheduleFragmentDirections.actionFullScheduleFragmentToStopScheduleFragment(
       stopName = it.stopName
   )
   view.findNavController().navigate(action)
})
recyclerView.adapter = busStopAdapter
  1. Por último, para actualizar una vista de lista, llama a submitList() y pasa la lista de paradas de autobús del modelo de vista.
// 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())
}
  1. Haz lo mismo en StopScheduleFragment. Primero, obtén una referencia al modelo de vista.
private val viewModel: BusScheduleViewModel by activityViewModels {
   BusScheduleViewModelFactory(
       (activity?.application as BusScheduleApplication).database.scheduleDao()
   )
}
  1. Luego, configura la vista del reciclador en onViewCreated(). Esta vez, solo debes pasar un bloque vacío (función) con {}. En realidad, no quieres que ocurra nada cuando se presionen las filas de esta pantalla.
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))
}
  1. Ahora que ya configuraste el adaptador, completaste la integración de Room en la app de Bus Schedule. Tómate un momento para ejecutar la app y deberías ver una lista de horarios de llegada. Si presionas una fila, deberías poder navegar a la pantalla de detalles.

9. Responde a los cambios de datos con Flow

Aunque tu vista de lista esté configurada para manejar de manera eficiente los cambios en los datos cada vez que se llame a submitList(), tu app aún no podrá administrar actualizaciones dinámicas. Para comprobarlo, intenta abrir el Inspector de bases de datos y ejecutar la siguiente consulta para insertar un nuevo elemento en la tabla de horarios.

INSERT INTO schedule
VALUES (null, 'Winding Way', 1617202500)

Sin embargo, verás que en el emulador no sucede nada. El usuario supondrá que los datos no están modificados. Para ver los cambios, deberás volver a ejecutar la app.

El problema es que List<Schedule> se muestra desde cada una de las funciones DAO solo una vez. Incluso si se actualizan los datos subyacentes, no se llamará a submitList() para actualizar la IU y, desde la perspectiva del usuario, no se verá nada.

Para solucionar este problema, puedes aprovechar la función de Kotlin flujo asíncrono (a menudo llamada simplemente flujo) que le permitirá al DAO emitir datos de forma continua desde la base de datos. Si se inserta, actualiza o borra un elemento, el resultado se enviará de vuelta al fragmento. Mediante una función llamada collect(),, puedes llamar a submitList() con el valor nuevo emitido desde el flujo, de modo que ListAdapter pueda actualizar la IU en función de los datos nuevos.

  1. Para usar el flujo en Bus Schedule, abre ScheduleDao.kt. A fin de convertir las funciones DAO para mostrar un Flow, simplemente cambia el tipo de datos que se muestra de la función getAll() a Flow<List<Schedule>>.
fun getAll(): Flow<List<Schedule>>
  1. También puedes actualizar el valor que se muestra de la función getByStopName().
fun getByStopName(stopName: String): Flow<List<Schedule>>
  1. También se deben actualizar las funciones en el modelo de vista que acceden al DAO. Actualiza los valores que se muestran en Flow<List<Schedule>> para fullSchedule() y scheduleForStopName().
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {

   fun fullSchedule(): Flow<List<Schedule>> = scheduleDao.getAll()

   fun scheduleForStopName(name: String): Flow<List<Schedule>> = scheduleDao.getByStopName(name)
}
  1. Por último, en FullScheduleFragment.kt, el busStopAdapter debería actualizarse cuando llames a collect() en los resultados de la consulta. Como fullSchedule() es una función de suspensión, se debe llamar desde una corrutina. Reemplaza la línea.
busStopAdapter.submitList(viewModel.fullSchedule())

Con este código, se usa el flujo que se muestra de fullSchedule().

lifecycle.coroutineScope.launch {
   viewModel.fullSchedule().collect() {
       busStopAdapter.submitList(it)
   }
}
  1. Haz lo mismo en StopScheduleFragment, pero reemplaza la llamada a scheduleForStopName() por lo siguiente:
lifecycle.coroutineScope.launch {
   viewModel.scheduleForStopName(stopName).collect() {
       busStopAdapter.submitList(it)
   }
}
  1. Una vez que hayas realizado los cambios anteriores, podrás volver a ejecutar la app para verificar que los cambios de datos se manejen en tiempo real. Una vez que se esté ejecutando la app, regresa al Inspector de bases de datos y envía la siguiente consulta para insertar una nueva hora de llegada antes de las 8:00 a.m.
INSERT INTO schedule
VALUES (null, 'Winding Way', 1617202500)

El elemento nuevo debe aparecer en la parte superior de la lista.

79d6206fc9911fa9.png

Ya terminaste con la app de Bus Schedule. Felicitaciones. Ahora tienes una base sólida para trabajar con Room. En la próxima ruta, profundizarás tus conocimientos de Room con una nueva app de ejemplo y aprenderás a guardar datos creados por el usuario en un dispositivo.

10. Código de solución

El código de la solución para este codelab se encuentra en el proyecto y módulo que se muestran a continuación.

  1. Navega a la página de repositorio de GitHub del proyecto.
  2. Verifica que el nombre de la rama coincida con el especificado en el codelab. Por ejemplo, en la siguiente captura de pantalla, el nombre de la rama es main.

1e4c0d2c081a8fd2.png

  1. En la página de GitHub de este proyecto, haz clic en el botón Code, el cual abre una ventana emergente.

1debcf330fd04c7b.png

  1. En la ventana emergente, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.

Abre el proyecto en Android Studio

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open.

d8e9dbdeafe9038a.png

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > Open.

8d1fda7396afe8e5.png

  1. En el navegador de archivos, ve hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run 8de56cba7583251f.png para compilar y ejecutar la app. Asegúrate de que compile como se espera.

11. Felicitaciones

Resumen:

  • Las tablas de una base de datos SQL se representan en Room mediante clases de Kotlin llamadas entidades.
  • El DAO proporciona métodos correspondientes a comandos SQL que interactúan con la base de datos.
  • ViewModel es un componente que prioriza el ciclo de vida que se usa para separar los datos de tu app de su vista.
  • La clase AppDatabase le indica a Room qué entidades usar, proporciona acceso al DAO y realiza cualquier configuración cuando crea la base de datos.
  • ListAdapter es un adaptador que se usa con RecyclerView y es ideal para manejar listas actualizadas de forma dinámica.
  • Flow es una función de Kotlin que muestra un flujo de datos y se puede usar con Room para garantizar que la IU y la base de datos estén sincronizadas.

Más información