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:
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:
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 expresionestype
: 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 |
Objetos | Llamadas a métodos, acceso a propiedades |
Colecciones | Acceso 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:
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.
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.
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.
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!