Data Binding 1: Expresiones, Eventos Y Observables

La librería de Data Binding te ayuda a crear layouts declarativos con el fin de minimizar el código necesario para vincular la lógica de tu aplicación y los views.

La idea es minimizar las asignaciones de valores de nuestros views del código y personalizar la forma en que manejos eventos de UI.

Aplicación Android De Ejemplo

A lo largo de las explicaciones que veamos iré mostrando códigos de la app que permitan esclarecer el tema tratado. El siguiente es el resultado final luego de aplicar todos los apartados:

Aplicación Android Tienda Virtual Ejemplo Data Binding

Se trata de una App con un caso de uso que muestra el detalle de un producto, permitiendo la selección de talla, color y cantidad de unidades. Con el fin de agregarlo al carrito de la tienda virtual.

Configurar La Librería De Data Binding En Android Studio

En tu proyecto de Android Studio, abre al archivo build.gradle del módulo donde deseas los beneficios de la librería Data Binding.

Busca el bloque android, agrégale el objeto dataBinding y asigna true a su propiedad enable:

android {
    // Demás código por defecto, omitido por comodidad
    dataBinding {
        enabled = true
    }
}

En seguida sincroniza el proyecto con la barra de sugerencias que te ofrecerá la acción Sync Now.

Convertir Layout Para Recibir Expresiones De Binding

El punto de entrada para que se construyan las clases de binding asociadas al layout es el elemento <layout>.

La idea es recubrir tu nodo principal con esta etiqueta. Por ejemplo, el contenido principal de la actividad del detalle del producto (content_main.xml) está recubierto así:

<?xml version="1.0" encoding="utf-8"?>
<layout 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"
    tools:showIn="@layout/activity_main">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/activity_padding"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Por otro lado, Android Studio puede hacer envolver el elemento automáticamente si clickeas la bombilla amarilla o haces Alt + Enter.

Elige Convert to data binding layout para que suceda:

Ejemplo de Convert to data binding layout

Definir Sección De Datos

La definición del origen de la información que será vinculada a nuestros views se declara con la etiqueta <data>. En su interior podemos declarar imports, variables e includes.

Por ejemplo, el objeto que representa los datos del producto es declarado como una variable:

<data>       
        <variable
            name="product"
            type="com.herpro.databinding1.Product" />
</data>

Variables

Representan a los datos que serán usados en las expresiones de binding pra los views del layout.

Su etiqueta es <variable> y requiere los campos:

  • name: Nombre asignado para referirse a ella en las expresiones
  • type: El tipo de dato (atómicos o referencias)

Ejemplo:

<data>
        <variable
            name="exampleText"
            type="String" />

        <variable
            name="show"
            type="Boolean" />
        
        <variable
            name="product"
            type="com.herpro.databinding1.Product" />
</data>

Imports

Los imports funcionan similar a los que vemos en Java o Kotlin. Ponen en conocimiento las clases designadas al archivo layout con el fin de usar sus propiedades en las expresiones.

Usa la etiqueta <import> para agregarlas. El atributo type contendrá el string del paquete. Y si deseas, puedes usar el atributo alias para darle un sobrenombre a la clase que te quede cómodo.

Ejemplo:

<data>
        <import type="android.view.View" />

        <import
            alias="Print"
            type="com.herpro.databinding1.Utils" />
</data>

Includes

Si tienes includes para simplificar tus layouts es posible pasarle las variables desde el layout principal donde se recolecta el valor inicial.

Para ello usa el namespace xmlns:bind="http://schemas.android.com/apk/res-auto" y el atributo bind para pasar la referencia de la variable con el formato "@{}".

Por ejemplo, en la app que seguimos tengo el layout principal activity_main.xml el cual contiene la App Bar. Dentro de este hago un <include> para el content_main.xml. Con el fin de pasarle el objeto del producto uso la sintaxis mencionada de esta forma:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:bind="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="product"
            type="com.herpro.databinding1.Product" />
    </data>


    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay"
                app:title="@{product.name}" />

        </com.google.android.material.appbar.AppBarLayout>

        <include
            android:id="@+id/main_content"
            layout="@layout/content_main"
            bind:product="@{product}" />


    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

Es importante que en el layout incluido también se declaren las variables para sostener el valor.

Crear Origen De Datos

La siguiente es la clase que representa al producto que inflará el layout propuesto al inicio.

public class Product {
    // Lectura
    private String id = "1001";
    private String name = "Camiseta Sencilla";
    private String vendorName = "Tienda La 80";
    private float discount = 0.3f;
    private float price = 20.0f;
    private String description = "Camiseta blanca con cuello redondo muy confortable." +
            " Productos 100% garantizados";
    private float rating = 4.5f;
    private int reviewsNumber = 24;
    private ArrayList<String> sizes = new ArrayList<>();
    private ArrayList<Integer> colors = new ArrayList<>();
    private String imageSource = "file:///android_asset/t-shirt.jpg";

    
    public Product() {
        sizes.add("S");
        sizes.add("M");
        sizes.add("L");
        sizes.add("XL");

        colors.add(Color.parseColor("#ffcdd2"));
        colors.add(Color.parseColor("#bbdefb"));
        colors.add(Color.parseColor("#ffe0b2"));
        colors.add(Color.parseColor("#fafafa"));
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getVendorName() {
        return vendorName;
    }

    public float getDiscount() {
        return discount;
    }

    public float getPrice() {
        return price;
    }

    public String getDescription() {
        return description;
    }

    public float getRating() {
        return rating;
    }

    public int getReviewsNumber() {
        return reviewsNumber;
    }

    public ArrayList<String> getSizes() {
        return sizes;
    }

    public ArrayList<Integer> getColors() {
        return colors;
    }

    public String getImageSource() {
        return imageSource;
    }
}

El objeto que usaremos es el producido por los valores por defecto asignados en línea con los atributos.

Veamos como vincularlo con el layout en la actividad…

Cambiar Inflado Por Data Binding

La librería genera una clase por cada layout que usa el binding. Estas nos permitirán acceder a la jerarquía de views y asignar las variables declaradas.

Si tu layout se llama activity_main.xml, entonces el nombre de la clase de binding es ActivityMainBinding. Como es evidente, la nomenclatura de acceso es NombreLayoutBinding (notación PascalCase).

Sabiendo esto observemos como se creó el binding en la actividad del producto:

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);

	ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
	Product product = new Product();
	binding.setProduct(product);
}

La clase DataBindingUtil nos ayuda a inflar el layout de la actividad, retornando el binding asociado con las variables declaradas en <data>, reemplazando así la antigua llamada a setContentView().

Usa métodos set*NombreVariable() para asignar la instancia del tipo correspondiente.

De esta forma ya estamos preparados para escribir expresiones en nuestros views que reciban los datos de nuestro objeto de producto.

Crear Expresiones En Layouts

El lenguaje es el que permite vincular los datos o manejar eventos en los atributos de los views.

Estas deben ser escritas en el valor del atributo como un string con la sintaxis "@{}".

Por ejemplo, setear la descripción del objeto Product al atributo android:text de un TextView:

android:text="@{product.description}"

El límite para construir las expresiones se basa en los siguientes operadores y palabras reservadas:

Nombre Expresión
Operadores aritméticos+, -, /, *, %
Operadores de relación ==, >, <, >=, <= (usa < para <)
Concatenación de strings+
Operadores lógicos&&, ||, &, |, ^,!
Operadores de bit+, -, !, ~, >>, >>>, <<
Operador condicional ?:
Prioridad de expresiones, conversión de tipos()
Operador de comprobación de tipos intanceof
ObjetosLlamadas a métodos, acceso a propiedades
ColeccionesAcceso mediante separador []
Recursos@dimen, @string, @plurals,@color, etc (ver tabla de accesos especiales para otros recursos)

Los siguientes son expresiones de ejemplo usadas en la app del detalle de producto.

Asignar Nombre De La Tienda

Setear el nombre del vendedor solo requiere de una acceso al atributo del nombre

android:text="@{product.vendorName}"

Simplemente usamos un acceso de propiedad. Cabe aclarar que getVendorName() produciría el mismo resultado.

Formatear Descuento Con Recurso String De Formato

Para el Text View de descuento queremos añadir un signo de menos al inicio y un porcentaje al final (ej. -30%) en la UI.

Para que ello suceda creamos un recurso string que añado dichos formatos:

<string name="format_discount">−%d%%</string>

La forma de acceder a este en una expresión es la siguiente:

 android:text="@{@string/format_discount((int)(product.discount*100))}"

En primer lugar accedemos al string formateado por su nombre como si se tratara de una función, donde el parámetro es un entero.

Debido a que product.discount es la fracción del porcentaje, multiplicamos por 100 y luego casteamos a entero

Mostrar/Ocultar TextView Del Descuento

Si el descuento es 0, entonces escondemos el view usando el operador ternario en una expresión que determine el valor de android:visibility.

<data>
    <import type="android.view.View" />
</data>
    ...
        
<TextView
            android:id="@+id/discount"
            android:visibility="@{product.getDiscount()>0?View.VISIBLE:View.GONE}" .../>

Formatear Precio Con Método Estático

En el caso del texto del precio, tenemos que mostrar un numero flotante con máximo 2 decimales, y si no tiene, entonces mostrar como entero.

Con este fin en mente se creó la clase Utils, donde está el método formatPrice(), encargado del formato:

public class Utils {
    public static String formatPrice(float price) {
        if (price % 1 == 0) {
            return String.format(Locale.getDefault(), "$%d", (int) price);
        } else {
            return String.format(Locale.getDefault(), "$%.2f", price);
        }
    }
}

Lo llamaremos en la expresión del Text View usando un import con el alias de Print:

<data>  
        <import type="android.view.View" />
        <import
            alias="Print"
            type="com.herpro.databinding1.Utils" />
        <variable
            name="product"
            type="com.herpro.databinding1.Product" />
</data>
    
        <TextView
            android:id="@+id/price"
            android:text="@{Print.formatPrice(product.price)}"
            .../>

El formato se vería de esta forma:

Asignar Estrellas En La RatingBar

Aquí usaremos el atributo android:rating que recibe un flotante para mostrar el progreso en el drawable de las estrellas:

android:rating="@{product.rating}"

El resultado:

RatingBar android:rating con Data Binding

Usar Plurals Para El Número De Reviews

Como ves en la captura de la imagen, el número de reseñas va en la UI acompañado de la palabra que indica si hay varias, una o ninguna.

Recurso Plurals con Data Binding

Para resolver esta preferencia idiomática en el Español he creado un plural que modifica la palabra dependiendo de la cantidad:

<plurals name="reviews">
        <item quantity="zero">No hay reseñas</item>
        <item quantity="one">(1 reseña)</item>
        <item quantity="many">(%d reseñas)</item>
        <item quantity="other">(%d reseñas)</item>
</plurals>

Y para referenciarlo en el atributo de texto anotamos su nombre y le pasamos como parámetros la cantidad de placeholders que existan (2 en este caso) :

android:text="@{@plurals/reviews(product.reviewsNumber,product.reviewsNumber)}"

Expresiones Para Manejar Eventos

Por otro lado, también es posible aplicar expresiones que apunten los controladores de las escuchas estándar originadas por los views a los nuestros.

El más común es android:onClick, el cual representa el controlador de la interfaz OnClickListener.

Aunque este es el único reconocido por el editor de Android Studio, puedes usar el nombre del método asociado a la escucha del evento.

Ejemplo: android:onCheckedChanged, android:onLongClick, android:onTextChanged, etc.

La librería de Data Binding hará todo el trabajo de instanciacion para redirigir el flujo a nuestro método personalizado.

Analicemos las dos formas que existen para llevarlo a cabo…

Ejecutar Referencias De Métodos

Usamos una expresión donde pasamos el nombre del método personalizado (accede con ::), asegurándonos de que tenga la misma firma del controlador estándar.

Por ejemplo, tenemos una escucha propia que contiene un método que deseamos se ejecute al hacer click:

public interface ExampleListener {
	void onExampleEvent(View v);
}

Luego declaramos la variable que contendrá el objeto del evento y lo referenciamos desde un Text View en su atributo onClick:

<data>
    <variable
            name="handler"
            type="com.herpro.databinding1.MainActivity.ExampleListener" />
</data>

<TextView
            android:id="@+id/textView"
            android:onClick="@{handler::onExampleEvent}" .../>

Finalmente bindeamos su instancia:

binding.setHandler(new ExampleListener() {
    @Override
    public void onExampleEvent(View v) {
        // Acciones
    }
});

Listener Bindings

Usamos este mecanismo si queremos evaluar alguna expresión cuando ocurre el evento. Lo único que debe coincidir con nuestro método personalizado es el valor de retorno. Los parámetros pueden diferir ya que usaremos expresiones lambda.

Teniendo esto en cuenta, analicemos como vincular los eventos de la selección del producto con la siguiente escucha:

public interface OnSelectProductListener {
	void onColorSelected(RadioGroup colors, Product product);

	void onDecrement();

	void onIncrement();

	void onAddToCart(String id, String size, int color, int unitsToBuy);
}

Seleccionar Color De Camisa

Para seleccionar el color de la camiseta desde el RadioGroup usamos el controlador onColorSelected() en el atributo android:onCheckedChanged:

<data>   
    <variable
            name="product"
            type="com.herpro.databinding1.Product" />
    <variable
            name="listener"
            type="com.herpro.databinding1.MainActivity.OnSelectProductListener" />
</data>
...
<RadioGroup
            android:id="@+id/colors"
            android:onCheckedChanged="@{(colors,id)->listener.onColorSelected(colors,product)}" />

Como ves, en la función lambda pasamos los parámetros en la firma de onCheckedChanged(RadioGroup group, int checkedId) con el fin de pasar al grupo de radios en la escucha junto al producto.

Se actualiza el color seleccionado en el producto

Cada vez que se cambia el color actualizamos el valor actual en el objeto del producto.

Incrementar/Disminuir Unidades A Comprar

Para incrementar y disminuir la cantidad de unidades vinculamos al atributo android:onClick de los botones los métodos onIncrement() y onDecrement() :

<ImageButton
                android:id="@+id/decrement_button"
                android:onClick="@{()->listener.onDecrement()}" />
<ImageButton
                android:id="@+id/increment_button"
                android:onClick="@{()->listener.onIncrement()}" />

Debido a que estos métodos no reciben parámetros, dejamos la expresión lambda vacía.

Añadir Producto Al Carrito De Compras

En el caso del botón de añadir al carrito usamos la variable del producto para pasar los datos necesarios en esta operación:

<Button
            android:id="@+id/add_to_cart_button"
            android:onClick="@{()->listener.onAddToCart(product)}"
            ... />

El resultado es la impresión de un Toast con los datos vitales para añadir al carrito.

Pasar Escucha En El Objeto De Binding

Finalmente le pasamos al objeto de binding una instancia de la escucha con la implementación de cada método:

binding.setListener(new OnSelectProductListener() {
            @Override
            public void onColorSelected(RadioGroup colors, Product product) {
                for (int i = 0; i < colors.getChildCount(); i++) {
                    RadioButton rb = (RadioButton) colors.getChildAt(i);
                    if (rb.isChecked()) {
                        product.selectedColor.set((Integer) rb.getTag());
                        break;
                    }
                }
            }

            @Override
            public void onDecrement() {
                product.unitsToBuy.decrement();
            }

            @Override
            public void onIncrement() {
                product.unitsToBuy.increment();
            }

            @Override
            public void onAddToCart(Product product) {
                String output = String.format(Locale.getDefault(),
                        "Añadir=>[%s, %s, %s, %d]",
                        product.getId(), product.selectedSize.get(),
                        product.selectedColor.get(), product.unitsToBuy.get());
                Toast.makeText(MainActivity.this, output, Toast.LENGTH_SHORT).show();
            }
        });

Actualizar UI Con Campos Observables

Debido a que deseamos que la clase de binding tenga datos actualizados de la selección de la talla, el color y el contador, es necesario usar campos observables.

Esta característica de Data Binding permite notificar el cambio de datos de un objeto a otros.

Para hacerlo, podemos usar clases prediseñadas de la librería que implementan interfaces de observación. Algunas de ellas son:

  • ObservableBoolean
  • ObservableInt
  • ObservableLong
  • ObservableFloat
  • ObservableDouble

En complemento, declara el atributo como final para que se haga el seguimiento del valor.

Observar Talla, Color Y Unidades

La talla, el color y las unidades debe estar actualizadas en cada interacción de usuario con las vistas relacionadas. Al presionar el botón de agregar al carro, se deberían pasar como parámetros para una resolución posterior (en esta oportunidad solo veremos un Toast con los valores).

Ya que no hay una clase primitiva observable para los strings, usaremos ObservableField<String> para notificar los cambios de la talla y color seleccionados.

En el caso del contador para las unidades, ObservableInt es un buen candidato. No obstante, como necesitamos un comportamiento de incrementos y disminuciones, crearemos una subclase de este con métodos que nos ayuden llamada CounterObservableInt:

public class CounterObservableInt extends ObservableInt {

    public CounterObservableInt(int value) {
        super(value);
    }

    public void increment() {
        set(get() + 1);
    }

    public void decrement() {
        set(get() <= 1 ? 1 : get() - 1);
    }
}

Y en la clase Product declararíamos los atributos:

public class Product {
    // Lectura
    ...

    // Escritura
    public final CounterObservableInt unitsToBuy = new CounterObservableInt();
    public final ObservableField<String> selectedSize = new ObservableField<>();
    public final ObservableInt selectedColor = new ObservableInt(1);

    public Product() {
        ...

        selectedSize.set(sizes.get(0));
        selectedColor.set(colors.get(0));
    }

    ...
}

Con esto realizado, cada que usemos los atributos en las expresiones del layout tendrán el valor actualizado.

Característica relevante para el Text View del contador que debe mostrar cambio cada que el usuario hace click en los botones de incremento/decremento:

<TextView
                android:id="@+id/units"
                android:text="@{String.valueOf(product.unitsToBuy)}"
                android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
                ... />

Luego de seguir todos estos pasos tendrás tu interfaz vinculada a tus modelos de datos gracias al Data Binding. Reduciendo la cantidad de código de asignación y mejorando la lectura de tus clases.

App Android de Ejemplo Data Binding

Descargar Código De La App

Descarga el proyecto Android Studio completo mientras a la misma vez me apoyas con la escritura de futuros tutoriales. ¡Realmente agradecería tu ayuda!

Descargar Código Android

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!