Fundamentos de Android: Data Binding

GoogleIO19

Hasta ahora hemos visto dos maneras para acceder a los elementos visuales del diseño (views) desde la actividad:
  1. El clásico método findViewById con la referencia del recurso del elemento como argumento, por ejemplo:
  2. val dadoBoton: Button = findViewById(R.id.dado_boton)
  3. Y el "método síntético" (synthetics) de las extensiones de Android de esta manera:
  4. // requiere import kotlinx.android.synthetic.main.activity_main.*
    val dadoBoton: Button = dado_boton
Sin duda la segunda opción resulta más elegante y práctica, y cada vez ha sido más usada tendiendo a sustituir a la primera. No obstante, realmente nunca ha sido una práctica formalmente recomendada; su API no fue construida por Google y es parte de las Extensiones creadas por JetBrains, y aunque Google en ocasiones también la ha usado, finalmente ha establecido que no es la mejor práctica (más información, en inglés, en The Argument Over Kotlin Synthetics).

Y aunque a este método se le han achacado problemas de seguridad y rendimiento, parece que en este criterio de Google ha podido influir el hecho de que no está disponible en Java, por lo que pone en cuestión la interoperabilidad de Kotlin con este lenguaje de programación. Pese a todo, en general sigue siendo una buena opción en Kotlin.

Ante esta situación han ido apareciendo algunas alternativas para resolver, con sus ventajas e inconvenientes, esta situación, como son Data Binding y Butterknife. Y aunque al parecer el sucesor definitivo será View Binding, que estará disponible en próximas versiones de Android Studio como anunció Yigit Boyar en Google I/O'19 (What's New in Architecture Components), de momento en esta entrada trataremos del uso de Data Binding, la opción que actualmente defiende Google.

Kotlin: How To Access Views

Hay que tener en cuenta que cada vez que se usa findViewById (y también synthetics) para buscar un elemento (view), el sistema Android debe atravesar en tiempo de ejecución toda la jerarquía de elementos para localizarlo, lo que no es un problema en aplicaciones con diseños pequeños, pero en aplicaciones mayores y complejas puede llevar el tiempo suficiente como para ralentizar notablemente la aplicación (el almacenamiento de views en variables puede ayudar, pero tampoco es la solución).

Una solución es crear un objeto que contenga una referencia a cada view y que este objeto, llamado objeto de enlace (binding object), pueda ser utilizado por toda la aplicación. Esta técnica se llama enlace o vínculo de datos (data binding) y se basa en que, una vez que se ha creado el objeto de enlace para la aplicación, se puede acceder a las views y a otros datos a través de ese objeto sin tener que atravesar la jerarquía de vistas o buscar los datos.

En general el vínculo de datos o data binding tiene los siguientes beneficios:
  • El código es más corto, más fácil de leer y más fácil de mantener que el código que usa findByView().
  • Los datos y los elementos de diseño están claramente separados.
  • El sistema Android solo atraviesa la jerarquía de vistas una vez para obtener cada vista, lo que ocurre durante el inicio de la aplicación y no en tiempo de ejecución cuando el usuario interactúa con la aplicación.
  • Se obtiene seguridad de tipo para acceder a las views, lo que significa que el compilador valida los tipos durante la compilación y arroja un error si intenta asignar el tipo incorrecto a una variable.

Para verlo con un ejemplo vamos a partir del código del proyecto ControlAcceso que vimos en Fundamentos de Android: comunicación entre actividades. Cocretamente, los archivos de las actividades y del diseño quedaron así:
// MainActivity.kt
package com.android.controlacceso

import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        entrar_btn.setOnClickListener {
            if (!esNuloVacio(user_input.text.toString()) && !esNuloVacio(pass_input.text.toString())) {
                val intento1 = Intent(this, LoginCheck::class.java)
                intento1.putExtra("usuario", user_input.text.toString())
                user_input.setText("")
                pass_input.setText("")
                startActivity(intento1)
            } else {
                Toast.makeText(
                    this, "ERROR: Usuario y contraseña requeridos.",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }
    }

    private fun esNuloVacio(str: String?): Boolean {
        return !(str != null && !str.trim().isEmpty())
    }
}
<!--- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout 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:orientation="vertical"
        tools:context=".MainActivity">

    <LinearLayout
            android:id="@+id/linearLayout"
            style="@style/estilo_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <TextView
                android:id="@+id/user_txt"
                style="@style/estilo_texto"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/margen_txt_end1"
                android:text="@string/usuario" />

        <EditText
                android:id="@+id/user_input"
                style="@style/estilo_texto"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ems="10"
                android:hint="@string/usuario"
                android:inputType="textPersonName" />
    </LinearLayout>

    <LinearLayout
            android:id="@+id/linearLayout2"
            style="@style/estilo_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:orientation="horizontal">

        <TextView
                android:id="@+id/pass_txt"
                style="@style/estilo_texto"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/margen_txt_end2"
                android:text="@string/contraseña" />

        <EditText
                android:id="@+id/pass_input"
                style="@style/estilo_texto"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ems="10"
                android:hint="@string/contraseña"
                android:inputType="textPassword" />
    </LinearLayout>

    <Button
            android:id="@+id/entrar_btn"
            style="@style/Widget.AppCompat.Button.Colored"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="@dimen/margen_linea"
            android:text="@string/btn_entrar" />

</LinearLayout>
// LoginCheck.kt
package com.android.controlacceso

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_login_check.*

class LoginCheck : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login_check)

        val bundle = intent.extras
        val user = bundle?.getString("usuario")

        login_txt.text = getString(R.string.bienvenido, user)

        button_cerrar.setOnClickListener {
            finish()
        }
    }
}
<!--- activity_login_check.xml -->
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        style="@style/estilo_linear"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".LoginCheck">

    <TextView
            android:id="@+id/acceso_txt"
            style="@style/estilo_texto"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margen_linea"
            android:text="@string/login_check"
            android:textAlignment="center" />

    <TextView
            android:id="@+id/login_txt"
            style="@style/estilo_texto"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margen_linea"
            android:text="@string/bienvenido"
            android:textAlignment="center" />

    <Button
            android:id="@+id/button_cerrar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margen_linea"
            android:text="@string/cerrar" />
</LinearLayout>

A partir de este código vamos a configurar el vínculo de datos (data binding) y lo vamos a usar para reemplazar las referencias a los elementos del diseño con llamadas al objeto de enlace o binding object.

Para usar data binding, lo primero que tenemos que hacer es habilitarlo en el archivo Gradle (por defecto no está habilitado porque aumenta el tiempo de compilación y puede afectar al tiempo de inicio de la aplicación).

Abrimos app/build.gradle (Module: app) y dentro de la sección de android añadimos una sección de dataBinding y lo habilitamos configurando enabled en true:
dataBinding {
    enabled = true
}

Entonces sincronizamos el proyecto desde el aviso que ha aparecido junto a la barra de herramientas con el texto Gradle files have changed... Sync Now. En caso de que no aparezca este aviso, puedes sincronizar desde File > Sync Project with Gradle Files.

Después, para trabajar con data binding debemos ajustar el diseño XML con una etiqueta <layout> que será la clase raíz que contendrá el resto de elementos y grupos de elementos, lo que permite que el binding object pueda reconocer los elementos que contiene. Para ello abrimos el archivo layout/activity_main.xml en vista Text y añadimos <layout> y </layout> como la etiqueta más externa alrededor del <LinearLayout>:
<layout>
   <LinearLayout ... >
   ...
   </LinearLayout>
</layout>

Si necesitas recolocar la sangría del código, lo puedes hacer desde Code > Reformat code. Como las declaraciones de espacio de nombres deben estar en la etiqueta más externa, las cortamos del <LinearLayout> y las pegamos en la nueva etiqueta <layout>:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            tools:context=".MainActivity">

Bien, ahora vamos a crear un objeto de enlace (binding object) en la actividad principal de modo que nos sirva para acceder a las views. Abrimos el archivo MainActivity.tk y antes de onCreate(), a nivel superior, creamos una varible (habitualmente llamada binding) para almacenar este objeto:
private lateinit var binding: ActivityMainBinding

El tipo de la variable binding, la clase ActivityMainBinding, es creado específicamente para esta actividad y su nombre se deriva del nombre del archivo de diseño, que es activity_main, más Binding. Al crear esta variable, Android Studio ha solicitado la importación de ActivityMainBinding, que se ve así:
import com.android.controlacceso.databinding.ActivityMainBinding

También tenemos que reemplazar la actual función setContentView() con una instrucción que crea el objeto de enlace y asocia, con el método setContentView de la clase DataBindingUtil (que tiene que ser importada), la actividad de MainActivity (this) con el diseño de activity_main:
//setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

Ahora ya podemos reemplazar en el código todas las llamadas a las views con referencias mediante el objeto de enlace (binding data), teniendo en cuenta que el compilador genera los nombres de las views en el objeto a partir de sus ID, transformando sus nombres en formato camelCase (por ejemplo, done_button será doneButton en el binding object). Así, en nuestro código tenemos la llamada a entrar_btn que la podemos remplazar por binding.entrarBtn de esta manera:
//entrar_btn.setOnClickListener {
binding.entrarBtn.setOnClickListener {

Repetimos con el resto de llamadas a los elementos del diseño, quedando el archivo MainActivity.tk así (se han mantenido comentadas las llamadas a las views):
package com.android.controlacceso

import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.android.controlacceso.databinding.ActivityMainBinding
//import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        //entrar_btn.setOnClickListener {
        binding.entrarBtn.setOnClickListener {
            //if (!esNuloVacio(user_input.text.toString()) && !esNuloVacio(pass_input.text.toString())) {
            if (!esNuloVacio(binding.userInput.text.toString()) && !esNuloVacio(binding.passInput.text.toString())) {
                val intento1 = Intent(this, LoginCheck::class.java)
                //intento1.putExtra("usuario", user_input.text.toString())
                intento1.putExtra("usuario", binding.userInput.text.toString())
                //user_input.setText("")
                binding.userInput.setText("")
                //pass_input.setText("")
                binding.passInput.setText("")
                startActivity(intento1)
            } else {
                Toast.makeText(
                    this, "ERROR: Usuario y contraseña requeridos.",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }
    }

    private fun esNuloVacio(str: String?): Boolean {
        return !(str != null && !str.trim().isEmpty())
    }
}

Como vemos, ya no es necesaria la importación de kotlinx.android.synthetic.

Aplicamos los mismos pasos para la actividad LoginCheck.kt y su archivo de diseño activity_login_check.xml:
  1. Etiqueta <layout> en el archivo de diseño como elemento raíz, con las correspondientes declaraciones de espacio de nombres.
  2. Creamos la varible que almacena el binding object (e importamos la clase correspondiente).
  3. Creamos el objeto de enlace y asociamos la actividad con su diseño.
  4. Remplazamos las llamadas a las views por referencias en el objeto.

Nota: si Android Studio advierte de algún error en el paso 2, y concretamente de un error como Unresolved reference: databinding..., intenta resolverlo actualizando los archivos generados desde Build -> Clean Project seguido de Build > Rebuild Project. Si no se soluciona, prueba con File > Invalidate Caches/Restart para realizar una limpieza más exhaustiva.

Aplicando estos cambios, el archivo LoginCheck.kt nos queda así:
package com.android.controlacceso

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.android.controlacceso.databinding.ActivityLoginCheckBinding

class LoginCheck : AppCompatActivity() {

    private lateinit var binding: ActivityLoginCheckBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_login_check)

        val bundle = intent.extras
        val user = bundle?.getString("usuario")

        binding.loginTxt.text = getString(R.string.bienvenido, user)

        binding.buttonCerrar.setOnClickListener {
            finish()
        }
    }
}

Ejecuta la aplicación en el emulador para comprobar que funciona. De esta manera hemos conseguido utilizar referencias de los elementos del diseño de una manera más sofisticada con binding. Además, en ocasiones podemos agrupar instrucciones binding usando la función apply{}:
// binding.userInput.setText("")
// binding.passInput.setText("")
binding.apply {
    userInput.setText("")
    passInput.setText("")
}

Comentarios

Entradas populares

Recursos gratis para aprender Kotlin

I/O: entrada y salida de datos en consola

Lectura y escritura de archivos