Persistencia de datos en Android con Room

5
25016

Í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/

5 COMENTARIOS

  1. 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

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad