Índice de contenidos
1. Introducción
Con este tutorial aprenderás a usar Room, una librería para manejar bases de datos SQLite en Android de una manera más segura. Además desarrollaremos un ejemplo utilizando Kotlin.
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: MacBook Pro 17’ (2,66 GHz Intel Core i7, 8GB DDR3)
- Sistema operativo: macOS Sierra 10.13.6
- Entorno de desarrollo: Android Studio 3.3
- Versión SDK mínima: 16
3. SQLite
3.1. SQLite
SQLite es un sistema de dominio público de gestión de bases de datos relacionales. La principal ventaja que presenta es que no funciona como un proceso independiente, sino que forma parte de la aplicación que lo utiliza. Por tanto, no necesita ser instalado independientemente, ejecutado o detenido, ni tiene fichero de configuración. Además sigue los principios ACID.
En Android es bastante útil, ya que permite la utilización de una base de datos local en el dispositivo que utilice nuestra aplicación de una manera relativamente ligera y sencilla.
3.2. Inconvenientes
SQLite es relativamente de bajo nivel, por lo que presenta ciertos riesgos. Su implementación requiere tiempo y esfuerzo, ya que se deben escribir las sentencias SQL de la base de datos. Por ello, si el modelo de datos sufre cambios, tendremos que modificar estas sentencias manualmente, con el riesgo que ello implica. Por si esto no fuera poco, estas sentencias no se comprueban durante la compilación, por lo que hay un importante riesgo de errores en tiempo de ejecución.
4. Room
4.1. Room
Room es una librería que abstrae el uso de SQLite al implementar una capa intermedia entre esta base de datos y el resto de la aplicación. De esta forma se evitan los problemas de SQLite sin perder las ventajas de su uso.
Room funciona con una arquitectura cuyas clases se marcan con anotaciones preestablecidas. Por otro lado, la mayoría de las consultas a la base de datos sí se comprueban en tiempo de compilación.
4.2. Arquitectura
Las partes de las que se compone Room son las siguientes:
- Entity: son clases que definen las tablas de la base de datos y de las entidades a utilizar.
- DAO: interfaces que definen los métodos utilizados para acceder a la base de datos.
- RoomDatabase: sirve de acceso a la base de datos SQLite a través de los DAOs definidos.
Además, es recomendable utilizar una clase intermedia a la cual denominamos Repository cuya finalidad es administrar las diferentes fuentes de datos.
4.3. Anotaciones
Para que se detecten qué clases tendrán que ser tratadas por esta librería y para indicar ciertas configuraciones debemos utilizar anotaciones. Las principales son las siguientes:
- @Database: para indicar que la clase será el Database. Además, dicha clase debería ser abstracta y heredar de RoomDatabase.
- @Dao: se utiliza para las interfaces de los DAOs.
- @Entity: indica que la clase es una entidad.
- @PrimaryKey: indica que el atributo al que acompaña será la clave primaria de la tabla. También podemos establecer que se asigne automáticamente si la incluimos así: @PrimaryKey(autoGenerate = true)».
- @ColumnInfo: sirve para personalizar la columna de la base de datos del atributo asociado. Podemos indicar, entre otras cosas, un nombre para la columna diferente al del atributo.
- @Ignore: previene que el atributo se almacene como campo en la base de datos.
- @Index: para indicar el índice de la entidad.
- @ForeingKey: indica que el atributo es una clave foránea relacionada con la clave primaria de otra entidad.
- @Embedded: para incluir una entidad dentro de otra.
- @Insert: anotación para los métodos de los DAOs que inserten en la base de datos.
- @Delete: anotación para los métodos de los DAOs que borren en la base de datos.
- @Update: anotación para los métodos de los DAOs que actualicen una entidad en la base de datos.
- @Query: anotación para un método del DAO que realice una consulta en la base de datos, la cual deberemos especificar.
5. Ejemplo de utilización de Room
Vamos a desarrollar un ejemplo para ver cómo utilizar Room. Para ello vamos a crear una agenda sencilla con contactos y su teléfono.
5.1. Incorporando Room a nuestro proyecto
Para seguir este tutorial, crea un nuevo proyecto de Android con una actividad vacía. Una vez tengamos nuestro proyecto, tenemos que añadir las dependencias de Room para usarlo.
Para ello, abrimos el fichero de propiedades de gradle y lo editamos. Este tutorial se desarrolla con Kotlin, por lo que tendremos que añadir la dependencia de kapt. Además, nuestro ejemplo seguirá la arquitectura MVVM por lo que vamos a añadir las dependencias para el ViewModel y LiveData, aunque no es necesario para utilizar Room. El resultado es el siguiente:
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { compileSdkVersion 28 defaultConfig { applicationId "com.autentia.tutorialroom" minSdkVersion 16 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation "android.arch.persistence.room:runtime:1.1.1" kapt "android.arch.persistence.room:compiler:1.1.1" implementation "android.arch.lifecycle:extensions:1.1.1" //ViewModel and LiveData implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }
5.2. Creando la Entity
Nuestra base de datos tendrá una tabla para los contactos. Por tanto, tenemos que crear una clase de tipo Entity.
@Entity(tableName = Contact.TABLE_NAME) data class Contact( @ColumnInfo(name = "phone_number") @NotNull val phoneNumber: String, @ColumnInfo(name = "first_name") @NotNull val firstName: String, @ColumnInfo(name = "last_name") val lastName: String? = null ) { companion object { const val TABLE_NAME = "contact" } @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "contact_id") var contactId: Int = 0 }
Como puedes ver, la entidad tiene cuatro atributos y, como ninguno tiene la etiqueta @Ignore, todos serán columnas de la tabla. La clave primaria se autogenerará y sólo el apellido permitirá valores nulos. Además, todos los nombres de las columnas están especificados con la anotación @ColumnInfo.
5.3. Creando el DAO
Como ya se ha indicado, el DAO será una interfaz que especifica los métodos con los que accederemos a la entidad en la base de datos. Aunque en nuestro caso sólo vamos a insertar y listar todos los elementos, también tienes las funciones para borrar y modificar. También hay que fijarse en que el método getOrderedAgenda() devuelve un objeto de tipo LiveData. Esto no es obligatorio, pudiendo devolver un Array o una Lista, pero como nuestro ejemplo seguirá una arquitectura MVVM vamos a hacerlo así.
@Dao interface ContactDao { @Insert fun insert(contact: Contact) @Update fun update(vararg contact: Contact) @Delete fun delete(vararg contact: Contact) @Query("SELECT * FROM " + Contact.TABLE_NAME + " ORDER BY last_name, first_name") fun getOrderedAgenda(): LiveData<List<Contact>> }
5.4. La RoomDatabase
Nuestra database será abstracta y seguirá el patrón singleton para que sea compartida por cualquier objeto que la utilice. Definimos una función que devolverá el DAO que queremos y en el método getInstance ordenaremos a Room que inicialice la instancia de la database si es null y luego la devolveremos.
@Database(entities = [Contact::class], version = 1) abstract class ContactsDatabase : RoomDatabase() { abstract fun contactDao(): ContactDao companion object { private const val DATABASE_NAME = "score_database" @Volatile private var INSTANCE: ContactsDatabase? = null fun getInstance(context: Context): ContactsDatabase? { INSTANCE ?: synchronized(this) { INSTANCE = Room.databaseBuilder( context.applicationContext, ContactsDatabase::class.java, DATABASE_NAME ).build() } return INSTANCE } } }
5.5. La clase Repository
Nuestro repositorio accederá a la base de datos para recuperar el DAO de los contactos y tendrá dos métodos, uno para insertar y otro para recuperar el LiveData. Además, implementaremos una clase privada para poder ejecutar la llamada de inserción en un hilo independiente, ya que no se permite realizarla en el hilo principal.
class ContactsRepository(application: Application) { private val contactDao: ContactDao? = ContactsDatabase.getInstance(application)?.contactDao() fun insert(contact: Contact) { if (contactDao != null) InsertAsyncTask(contactDao).execute(contact) } fun getContacts(): LiveData<List<Contact>> { return contactDao?.getOrderedAgenda() ?: MutableLiveData<List<Contact>>() } private class InsertAsyncTask(private val contactDao: ContactDao) : AsyncTask<Contact, Void, Void>() { override fun doInBackground(vararg contacts: Contact?): Void? { for (contact in contacts) { if (contact != null) contactDao.insert(contact) } return null } } }
5.6. Accediendo a Room desde el resto de la app
Llegados a este punto, ya tenemos Room implementado, por lo que ahora vamos a desarrollar el resto de la aplicación para acceder a la base de datos y manipular su contenido. Para empezar, vamos a desarrollar el View Model, que instanciará la clase ContactsRepository para recuperar el LiveData e insertar contactos.
class ContactsViewModel(application: Application) : AndroidViewModel(application) { private val repository = ContactsRepository(application) val contacts = repository.getContacts() fun saveContact(contact: Contact) { repository.insert(contact) } }
Para seguir, modificaremos el layout activity_main.xml que encontramos dentro de la carpeta res>layout, en el cual incluiremos tres campos de texto y un botón para añadir contactos. Por otro lado, tendremos un TextView para poder mostrar los contactos, aunque también podríamos hacerlo por ejemplo con un ListView. El contenido debe quedar así:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.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"> <EditText android:id="@+id/fistName_editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" android:hint="Nombre" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <EditText android:id="@+id/lastName_editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" android:hint="Apellido" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/fistName_editText" /> <EditText android:id="@+id/phone_editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" android:hint="Teléfono" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/lastName_editText" /> <Button android:id="@+id/addContact_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Añadir" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/phone_editText" /> <TextView android:id="@+id/contacts_textView" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/addContact_button" android:gravity="center"/> </android.support.constraint.ConstraintLayout>
Por último, vamos con la clase MainActivity. Esta clase debe observar el LiveData del ViewModel para mostrar los cambios y añadir un listener al botón para añadir el contacto cuando se pulse.
class MainActivity : AppCompatActivity() { private lateinit var contactsViewModel: ContactsViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) contactsViewModel = run { ViewModelProviders.of(this).get(ContactsViewModel::class.java) } addContact_button.setOnClickListener { addContact() } addObserver() } private fun addObserver() { val observer = Observer<List<Contact>> { contacts -> if (contacts != null) { var text = "" for (contact in contacts) { text += contact.lastName + " " + contact.firstName + " - " + contact.phoneNumber + "\n" } contacts_textView.text = text } } contactsViewModel.contacts.observe(this, observer) } private fun addContact() { val phone = phone_editText.text.toString() val name = fistName_editText.text.toString() val lastName = if (lastName_editText.text.toString() != "") lastName_editText.text.toString() else null if (name != "" && phone != "") contactsViewModel.saveContact(Contact(phone, name, lastName)) } }
Con esto hemos terminado el tutorial, por lo que podemos ejecutar nuestra aplicación y añadir nuevos contactos que se mostrarán debajo del botón.
¡Muchas gracias por haber leído hasta aquí!
6. Referencias
http://www.sqlitetutorial.net/what-is-sqlite//a>
https://developer.android.com/reference/androidx/room/Room
https://www.sqlite.org/index.html
https://developer.android.com/training/data-storage/room/
Genial, voy a probarlo, muchas gracias!
Muy bien explicado, felicitaciones y gracias
Excelente tutorial!!!
Hola, muy bueno el tutorial, bastante simple la explicación, esto es lo que uno espera. Tengo una duda, en el repositorio por qué no creaste métodos para borrar contactos? Sería bueno explicar cómo se haría la eliminación de un contacto o la actualización. Pero la duda principal es si no de debe incluir métodos de eliminar en el repositorio, he visto varios ejemplos que no lo hacen y no sé si es porque no se debe. Muchas gracias
Gracias por compartirlo, excelente trabajo me ayudo bastante