Saltar la navegación

4.5.1. Ejemplo

Vamos a desglosar la explicación del código anterior por documentos, desde Gradle, hasta el MainActivity que ejecuta la aplicación:

build.gradle

Este archivo debe modificarse añadiendo varias líneas en la zona plugins y en dependencies:

id 'kotlin-kapt' // En plugins

// En dependencies
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.room:room-ktx:2.4.3'
kapt 'androidx.room:room-compiler:2.4.3'

Esto cargará en nuestra aplicación el plugin kapt y las dependencias para utilizar los métodos de Room.

Además, incorporamos los métodos del RecyclerView que usaremos para mostrar la información.

TaskEntity.kt

Este archivo contiene las entidades (tablas) en forma de data class.

Cada entidad en Room debe tener la anotación @Entity delante de la definición de la clase. Además, se le puede añadir parámetro como:

  • tableName: nombre de la tabla
  • primaryKeys: para combinar dos o más campos como campo clave.
  • ignoreColumns: para ignorar algún campo que no deseemos cargar.

Debe existir una clase por cada entidad.

@Entity(tableName = "task_entity")
data class TaskEntity (
    @PrimaryKey(autoGenerate = true)
    var id:Int = 0, // Id de la tarea
    var name:String = "", // Nombre de la tarea
    var isDone:Boolean = false // Booleano que indica si la tarea está hecha o no
)

La anotación @Entity la utilizamos para añadirle un nombre a nuestra entidad como tabla de la base de datos.

La anotación @PrimaryKey (autoGenerate = true) está diciendo que la variable id es un valor que se autogenera al crear un objeto de esta clase y que no podrá repetirse.

TaskDao.kt

La interfaz TaskDao será la primera capa sobre la base de datos y encargada de la comunicación con esta mediante sentencias SQL. Hay que crear una por cada entidad.

Bajo la notación @, se declara la función que se ejecutará en cada caso. Estas funciones o métodos, deben declararse como suspend para poder ejecutarlos en hilos diferentes al principal ya que, las lecturas/escrituras a bases de datos, consumen muchos recursos.

Los objetos DAO facilitan mucho el acceso a la BD. Aquí se declararán los métodos que interactuarán con las tablas:

  • @Query: Se hacen consultas directamente a la base de datos usando SQL.
  • @Insert: Se usará para insertar entidades a la base de datos, a diferencia de las @Query no hay que hacer ningún tipo de consulta, sino pasar el objeto a insertar.
  • @Update: Actualizan una entidad ya insertada. Solo tendremos que pasar ese objeto modificado y ya se encarga de actualizarlo. ¿Cómo sabe que objeto hay que modificar? Pues por nuestro id, la PrimaryKey. Si usamos onConflict, podremos decidir la acción en caso de conflicto: Ignore, Abort o Replace
  • @Delete: Como su propio nombre indica borra de la tabla un objeto que le pasemos.
@Dao
interface TaskDao {
    @Query("SELECT * FROM task_entity")
    suspend fun getAllTasks(): MutableList< TaskEntity>  // Función que devuelve todas las tareas de la base de datos en una lista Mutable.

    @Insert
    suspend fun addTask(taskEntity : TaskEntity):Long    // Función que añade una tarea, la que se pasa por parámetro, y devuelve el id insertado. 
// Devuelve Long porque la cantidad de datos guardada puede ser muy alto. @Query("SELECT * FROM task_entity where id like :id") suspend fun getTaskById(id: Long): TaskEntity // Función que busca tareas por id (debe ser Long, no Int) @Update suspend fun updateTask(task: TaskEntity):Int // Función que actualiza una tarea y devuelve @Delete suspend fun deleteTask(task: TaskEntity):Int // Función que borra una tarea y devuelve }

Estas funciones se declararán en el MainActivity.

TaskDatabase.kt

Esta clase nos permite la creación de la Base de Datos.

@Database(entities = arrayOf(TaskEntity::class), version = 1)
abstract class TasksDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
}

Como se puede observar, a la notificación @Database se le pasa como parámetro un array de Entidades (tipo clase) y se especifica la versión 1 de la base de datos (en el futuro, si se amplia la base de datos, se puede crear otra versión y el programa se adaptará a la base de datos adecuada)

El objeto base de datos TasksDatabase hereda la estructura Room y contiene una única función, taskDao() que devuelve la interface TaskDao encargada del manejo de la base de datos.

MisNotasApp.kt

Esta clase va a extender de Application() y será lo primero en ejecutarse al abrirse la aplicación.

class MisNotasApp: Application() {
    companion object {
        lateinit var database: TasksDatabase
    }
    override fun onCreate() {
        super.onCreate()
        MisNotasApp.database =  Room.databaseBuilder(this, TasksDatabase::class.java, "tasks-db").build()
    }
}

La instancia database necesita tres parámetros:

  1. el contexto (this),
  2. la clase de nuestra base de datos (TasksDatabase) declarada de forma global y accesible de forma estática,
  3. el nombre que le pondremos a la base de datos, en este caso, "tasks-db".

Para que esta clase se lance al abrir la app debemos ir al AndroidManifest.xml y añadir android:name=".MisNotasApp" dentro de la etiqueta.

Los objetos y variables declarados dentro de un companion object, pueden ser llamadas simplemente usando la clase y el nombre del objeto o variable, sin tener que instanciar la clase.

activity_main.xml

< ?xml version="1.0" encoding="utf-8"?>
< RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/background_light"
    tools:context="com.cursokotlin.misnotas.UI.MainActivity">
    < android.support.v7.widget.RecyclerView
        android:id="@+id/rvTask"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rlAddTask"/>
    < RelativeLayout
        android:id="@+id/rlAddTask"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:elevation="10dp"
        android:layout_margin="10dp"
        android:background="@android:color/white">
        < EditText
            android:id="@+id/etTask"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="añade una tarea"
            android:layout_alignParentLeft="true"
            android:layout_toLeftOf="@+id/btnAddTask"
            />
        < Button
            android:id="@+id/btnAddTask"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:text="Añadir"/>
    < /RelativeLayout>
< /RelativeLayout>

item_task.xml

< ?xml version="1.0" encoding="utf-8"?>
< LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_margin="10dp">
    < CheckBox
        android:id="@+id/cbIsDone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="10dp"
        android:layout_marginEnd="10dp" />
    < TextView
        android:id="@+id/tvTask"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textStyle="bold"
        android:textSize="18sp"
        tools:text="Test"/>
< /LinearLayout>

TaskAdapter.kt

Adapter es una herramienta que proporciona el sistema operativo Android, con la cual tenemos la posibilidad de transformar una cosa en otra diferente. De manera que podríamos decir que hace referencia a una especificación que realizamos en el programa, la cual permite que la información almacenada por medio de código se adapte a lo que el usuario verá en la pantalla.

Cuando creamos un RecyclerViewAdapter, la clase padre nos pide que debemos configurar tres métodos diferentes, los cuales son:

  • getItemCount: este método le dice al RecyclerView la cantidad de vistas que tenemos que renderizar. En este caso, el método getItemCount le indicará el tamaño de los elementos que están almacenados en la lista.
  • onCreateViewHolder: este es otro de los métodos que nos pide la clase padre. En pocas palabras, es el que va a funcionar como el contenedor en el que estaría la vista.
  • onBindViewHolder: este es el último de los métodos, el cual nos devuelve un View Holder con el fin de renderizarle la posición.

Crearemos una nueva clase, TaskAdapter, a la que le pasaremos 3 parámetros:

  1. la lista de tareas que tenemos almacenadas en nuestra base de datos,
  2. función que se ejecuta al pulsar el checkbox (check)
  3. función que se ejecuta al pulsar la tarea (delete)

Estas funciones nos permitirán recuperar el evento del click en cada una de las celdas. Devuelve la vista de elementos en la lista.

class TasksAdapter(
    val tasks: List< TaskEntity>,                   // Objeto Lista de tareas
    val checkTask: (TaskEntity) -> Unit,            // chequeo de tarea
    val deleteTask: (TaskEntity) -> Unit            // borrado de tarea
) : RecyclerView.Adapter< TasksAdapter.ViewHolder>() {    // Devuelve la vista

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {     // Muestra la vista (holder) y cada tarea de la lista (position)
        val item = tasks[position]                                         // Extrae la tarea de la lista
        holder.bind(item, checkTask, deleteTask)                           // Muestra el item en la vista (ver más adelante)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {    // Contenedor de la vista (holder) y la posición de la tarea en la lista (position)
        val layoutInflater = LayoutInflater.from(parent.context)                       // Se instancia el Layout para la vista
        return ViewHolder(layoutInflater.inflate(R.layout.item_task, parent, false))   // Devuelve la vista inflando el layout del item
    }

    override fun getItemCount(): Int {
        return tasks.size     // Devuelve el número de tareas de la lista
    }

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {     // Clase con la vista
        val tvTask = view.findViewById< TextView>(R.id.tvTask)         // instancia del Textview de la vista
        val cbIsDone = view.findViewById< CheckBox>(R.id.cbIsDone)     // instancia del Checkbox de la vista

        fun bind(                                   // función que une los elementos en la vista y prepara los listeners
            task: TaskEntity,
            checkTask: (TaskEntity) -> Unit,
            deleteTask: (TaskEntity) -> Unit
        ) {
            tvTask.text = task.name
            cbIsDone.isChecked = task.isDone
            cbIsDone.setOnClickListener { checkTask(task) }
            itemView.setOnClickListener { deleteTask(task) }
        }
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {

    lateinit var recyclerView: RecyclerView
    lateinit var adapter: TasksAdapter
    lateinit var tasks: MutableList< TaskEntity>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tasks = ArrayList()         // Se prepara la lista
        getTasks()                  // Se carga la lista de tareas a través del DAO
        findViewById< Button>(R.id.btnAddTask).setOnClickListener {
            addTask(TaskEntity(name = findViewById< EditText>(R.id.etTask).text.toString()))}
    }

    fun clearFocus(){
        findViewById< EditText>(R.id.etTask).setText("") // Borra el texto en el EditText
    }

    fun Context.hideKeyboard() {    // Oculta el teclado de texto
        val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
        inputMethodManager.hideSoftInputFromWindow(currentFocus?.windowToken, 0)
    }

    fun getTasks()= runBlocking {       // Corrutina que saca de la base de datos la lista de tareas
        launch {                        // Inicio del hilo
            tasks = MisNotasApp.database.taskDao().getAllTasks()    // Se carga la lista de tareas
            setUpRecyclerView(tasks)        // se pasa la lista a la Vista
        }
    }

    fun addTask(task:TaskEntity)= runBlocking{  // Corrutina que añade una tarea a la lista
        launch {
            val id = MisNotasApp.database.taskDao().addTask(task)   // Inserta una tarea nueva
            val recoveryTask = MisNotasApp.database.taskDao().getTaskById(id)   // Recarga la lista
            tasks.add(recoveryTask) // Añade al final de la lista, el nuevo
            adapter.notifyItemInserted(tasks.size)  // El adaptador notifica que se ha insertado
            clearFocus()        // Se elimina el texto del et ... 
            hideKeyboard()      // y se oculta el teclado
        }
    }

    fun updateTask(task: TaskEntity) = runBlocking{
        launch {
            task.isDone = !task.isDone  // Marca o desmarca el checkbox
            MisNotasApp.database.taskDao().updateTask(task) // Actualiza en la base de datos
        }
    }

    fun deleteTask(task: TaskEntity)= runBlocking{
        launch {
            val position = tasks.indexOf(task)  // Busca la posición de la tarea en la lista...
            MisNotasApp.database.taskDao().deleteTask(task) // ... y la borra de la base de datos.
            tasks.remove(task)      // Finalmente, la elimina de la lista
            adapter.notifyItemRemoved(position) // El adaptador notifica que se ha eliminado la tarea
        }
    }

    fun setUpRecyclerView(tasks: List< TaskEntity>) {    // Método que muestra la vista usando el adaptador
        adapter = TasksAdapter(tasks, { updateTask(it) }, {deleteTask(it)})
        recyclerView = findViewById(R.id.rvTask)
        recyclerView.setHasFixedSize(true)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter
    }
}