Data Binding 2: Binding Adapters

Resumen: en este tutorial, aprenderás a usar Binding Adapters para personalizar la lógica de asignación de un valor en un atributo de un view.

Seguido verás cómo usar Binding Methods para redireccionar los métodos set*() de atributos cuyo nombre no coincide con estos.

También aprenderás a convertir datos en las expresiones de vinculación con Binding Converters.

No olvides leer la parte 1 antes:

Binding Methods

Un binding method o método de vinculación soluciona el escenario en que la librería de Data Binding no encuentra el setter de un atributo.

¿Por qué sucede esto?

Porque la librería busca el método con la convención de nombrado setNombreAtributo() para ejecutarlo y en algunos casos esto no se cumple.

Ejemplo:

Para android:tint en un ImageView la librería espera encontrar setTint(), pero este no existe. El método es setImageTintList().

O con app:srcCompat espera setSrcCompat(), pero resulta que es setImageResource().

Por lo que si intentas usar el lenguaje de expresión en estos campos no habrá resultados.

Solución:

Crear una anotación @BindingMethods que contenga anotaciones @BindingMethod describiendo la redirección del método set en el atributo.

@androidx.databinding.BindingMethods({
        @BindingMethod(type = ImageView.class,
                attribute = "android:tint",
                method = "setImageTintList"),
        @BindingMethod(
                type = ImageView.class,
                attribute = "app:srcCompat",
                method = "setImageResource")
})
public class BindingMethods {
}

Como ves, la clase BindingMethods tiene dos anotaciones para los casos mencionados anteriormente.

@BindingMethod necesita la clase donde está el atributo, el atributo y el método que será interpretado como setter (type, attribute y method)

Binding Adapters

Un Binding Adapter te permite personalizar la lógica con la que un método set se ejecuta para un atributo.

¿Cómo hacerlo?

Dentro de una clase añade un método estático público de retorno void y anótalo con @BindingAdapter.

public class BindinAdapters {
    
    @BindingAdapter("nombre_atributo")
    public static void setNombreAtributo(View v, int otroParametro){
        // acción set*()
    }
}

La anotación debe recibir un parámetro asociado al view por editar.

El método debe recibir como primer parámetro el tipo de view a modificar.

Y si lo requieres añade más parámetros de los cuales dependa la asignación.

Ejemplo:

Mostrar/ocultar un view que representa la ausencia de datos con el atributo android:visibility:

@BindingAdapter("android:visibility")
    public static void showEmptyState(View v, boolean show) {
        v.setVisibility(show ? View.VISIBLE : View.GONE);
    }

Luego pasaríamos el parámetro show en una expresión de binding, suponiendo que existe una variable items del tipo List, evaluando si no tiene items:

android:visibility="@{items.size()==0}"

Binding Adapter Con Múltiples Parámetros

Los adaptadores también definir parámetros adicionales para el view con el fin de personalizar aún más la lógica de asignación.

Ejemplo:

Setear en un TextView la información de un paciente a través de su nombre, fecha de nacimiento e historial clínico:

@BindingAdapter({"name", "birth_date", "medical_records"})
    public static void setBio(TextView textView, String name, Date birthDate, List<MedicalRecord> records) {
        StringBuilder sb = new StringBuilder("Nombre: ");
        sb.append(name);
        sb.append("n");
        sb.append("Edad: ");
        sb.append(String.valueOf(AgeCalculator.calculateAge(birthDate)));
        sb.append("n");
        sb.append("Historial Médico: n");

        for (MedicalRecord record : records) {
            sb.append("-"+record.getDescription()+"n");
        }

        textView.setText(sb.toString());
    }

Como ves, la anotación recibe tres parámetros sin namespace, los cuales serán usados en el text view.

Los parámetros de la anotación deben coincidir con la cantidad de los del método sin contar la instancia del view.

La idea del ejemplo es crear un String formateado con cada parámetro y asignarlo con setText() al final. La forma de pasarlo en XML sería:

<TextView            
            app:name="@{patient.name}"
            app:birth_date="@{patient.birthDate}"
            app:medical_records="@{patient.medicalRecords}"/>

Usamos el app como namespace y escribimos el nombre del parámetro del adaptador y asignamos los valores de nuestras variables.

Binding Converters Personalizados

Permiten convertir de un tipo a otro en las expresiones de binding, dependiendo de la lógica que establezcamos.

La librería buscará automáticamente estos convertidores y los aplicará.

Para definirlos anota con @BindingConversion a un método publico estático en alguna clase. Especifica como parámetro el tipo entrante y el retorno como el tipo resultante.

public class BindingConverters {

    @BindingConversion
    public static Tipo1 tipo1ATipo2 (Tipo2 tipo2){
        // lógica de conversión
        return tipo1;
    }
}

Ejemplos De Binding Adapters, Methods Y Converters

Usaremos una app de ejemplo que representa la creación de una cuenta en un servicio hipotético.

Ejemplo de Binding Adapters

El proyecto consta de una sola actividad (MainActivity) y tres clases para los elementos de binding:

El layout de la actividad sin binding es el siguiente:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    android:padding="16dp"
    tools:context=".ui.MainActivity">

    <ImageView
        android:id="@+id/create_account_image"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        app:layout_constraintBottom_toBottomOf="@+id/welcome_text"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/name_field"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:ems="10"
        android:hint="@string/name_field_text"
        android:inputType="textPersonName"
        app:layout_constraintBottom_toTopOf="@+id/email_field"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/welcome_text" />

    <EditText
        android:id="@+id/email_field"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:ems="10"
        android:hint="@string/email_field_text"
        android:inputType="textEmailAddress"
        app:layout_constraintBottom_toTopOf="@+id/password_field"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/name_field" />

    <EditText
        android:id="@+id/password_field"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:ems="10"
        android:hint="@string/password_field_hint"
        android:inputType="textPassword"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/email_field" />

    <Button
        android:id="@+id/sign_up_button"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="@string/sign_up_button_text"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/industry_menu" />

    <TextView
        android:id="@+id/have_account_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:text="@string/login_support_text"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        app:layout_constraintBottom_toTopOf="@+id/login_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/sign_up_button"
        app:layout_constraintVertical_bias="1.0" />

    <Button
        android:id="@+id/login_button"
        style="@style/Widget.AppCompat.Button.Borderless"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/login_button_text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent" />

    <Spinner
        android:id="@+id/industry_menu"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/industry_label"
        tools:entries="@tools:sample/us_zipcodes" />

    <TextView
        android:id="@+id/create_account_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/create_account_text"
        android:textAppearance="@style/TextAppearance.AppCompat.Title"
        android:textColor="@android:color/black"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/welcome_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/create_account_title"
        tools:text="@string/day_message" />

    <ImageView
        android:id="@+id/time_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        app:layout_constraintBottom_toBottomOf="@+id/create_account_title"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toEndOf="@+id/create_account_title"
        tools:srcCompat="@drawable/ic_day" />

    <TextView
        android:id="@+id/industry_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="@string/industry_label"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/password_field" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.6" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:visibility="gone" />

    <androidx.constraintlayout.widget.Group
        android:id="@+id/group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="have_account_label,password_field,create_account_image,
            @+id/name_field,time_icon,create_account_title,industry_label,welcome_text,sign_up_button,
            industry_menu,guideline,email_field,login_button,name_field"
        tools:layout_editor_absoluteX="16dp"
        tools:layout_editor_absoluteY="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

La previsualización mostraría lo siguiente:

Como en la parte 1, conviértelo en un layout para Data Binding y reemplaza el inflado de la actividad.

Usar Un Binding Method Para app:srcCompat

Si deseamos asignar dinámicamente el drawable para los vectores del tiempo, necesitamos redirigir al atributo app:srcCompat hacia el método setImageResource():

@androidx.databinding.BindingMethods({
        @BindingMethod(
                type = ImageView.class,
                attribute = "srcCompat",
                method = "setImageResource")
})
public class BindingMethods {
}

Crear Binding Adapter Para Cargar Imagen Con Glide

Cargaremos la imagen del logo desde un drawable con la librería Glide. La idea es asignarle el resultado a create_account_image.

El binding adapter requiere el identificador entero del drawable, por lo que ese será el parámetro a usar:

@BindingAdapter("drawable")
    public static void loadLogo(ImageView imageView, Drawable drawable) {
        Glide.with(imageView).load(drawable).into(imageView);
    }

En seguida pasa la referencia del drawable que tenemos en el proyecto usando el operador @drawable.

<ImageView
            android:id="@+id/create_account_image"            
            app:drawable="@{@drawable/create_account_image}"
/>

Crear Binding Adapter Para Elegir El Vector

Dependiendo de la variable booleana day, así mismo usaremos un icono de sol u otro de luna.

<variable
            name="day"
            type="Boolean" />

El adaptador se vería así:

@BindingAdapter("srcCompat")
    public static void setSrcCompat(ImageView imageView, boolean day) {
        imageView.setImageDrawable(day ?
                ContextCompat.getDrawable(imageView.getContext(), R.drawable.ic_day) :
                ContextCompat.getDrawable(imageView.getContext(), R.drawable.ic_night)
        );
    }

Si el valor es verdadero, cargaremos el drawable ic_day, de lo contrario cargaremos ic_night; la asignación la realizamos con setImageDrawable().

La expresión de binding en el view sería:

<ImageView
            android:id="@+id/time_icon"
            app:srcCompat="@{day}" />

Crear Binding Adapter Para Color De Vector

Los vectores que representan la noche y el día tienen un background de color negro. Cambiaremos el tinte del vector a través del atributo android:tint dependiendo de si es día o noche.

@BindingAdapter("android:tint")
    public static void setTint(ImageView imageView, boolean day) {
        ImageViewCompat.setImageTintList(
                imageView,
                ColorStateList.valueOf(
                        day ?
                                ContextCompat.getColor(imageView.getContext(), R.color.day_color) :
                                ContextCompat.getColor(imageView.getContext(), R.color.night_color)
                ));
    }

Recibimos el mismo parámetro booleano y referenciamos a android:tint. La idea es usar ImageViewCompat.setImageTintList() para asignar el ColorStateList que resulte de day_color o night_color.

Al pasar el valor tenemos:

<ImageView
            android:id="@+id/time_icon"            
            android:tint="@{day}" />

Crear Binding Adapter Para Del Spinner

Asignar los items del Spinner se logra con android:entries y la variable industry_options.

<data>

        <import type="java.util.List" />
        <variable
            name="industry_options"
            type="List<String>" />
    </data>

Debido a que no existe un método set para este, crearemos un binding adapter que genere la asignación:

@BindingAdapter("android:entries")
    public static void setEntries(Spinner spinner, List<String> entries){
        ArrayAdapter<String> adapter = new ArrayAdapter<>(
                spinner.getContext(),
                android.R.layout.simple_spinner_item,
                entries
        );
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        spinner.setAdapter(adapter);
    }

La siguiente es la declaración XML:

<Spinner
            android:id="@+id/industry_menu"            
            android:entries="@{industry_options}" />

Crear Binding Converter De Booleanos A Enteros

La visibilidad requiere de banderas enteras que representan el estado del view. Si deseamos pasar un booleano en su lugar para el uso, añadimos un conversor que determine la correlación de valores:

public class BindingConverters {
    @BindingConversion
    public static int booleanToVisibility(boolean show) {
        return show ? View.VISIBLE : View.GONE;
    }
}

Como ves, positivo es VISIBLE y negativo GONE.

Si te fijas, nuestro layout tiene un grupo que sostiene todos los views de la jerarquía para que compartan su visibilidad.

En el momento en que se de click en el botón de registro, se mostrará una progress bar y los demás views desaparecerán.

Para lograr esto, usamos la variable booleana show en android:visibility del grupo y la barra de progreso de la siguiente forma:

<ProgressBar
            android:id="@+id/progressBar"
            android:visibility="@{!show}" />

        <androidx.constraintlayout.widget.Group
            android:id="@+id/group"
            android:visibility="@{show}" />

Al pasar el valor booleano, nuestro converter hará el trabajo de asignación.

Binding En La Actividad

Finalmente en la actividad ligaremos los parámetros:

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding mBinding;

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

        // vincular root del layout
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        // crear adaptador de spinner
        ArrayList<String> industryOptions = new ArrayList<>();
        industryOptions.add("Economía");
        industryOptions.add("Computación");
        industryOptions.add("Bienes raíces");
        industryOptions.add("Salud");

        // ligar variables
        mBinding.setDay(getHourOfDay());
        mBinding.setIndustryOptions(industryOptions);
        mBinding.setHandler(this);
        mBinding.setShow(true);
    }

    private boolean getHourOfDay() {
        // obtener hora del día
        Calendar c = Calendar.getInstance();
        int hourOfDay = c.get(Calendar.HOUR_OF_DAY);
        return hourOfDay > 0 && hourOfDay < 18;
    }

    public void signUp(View button) {
        mBinding.setShow(false);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mBinding.setShow(true);
            }
        }, 3000);
    }
}

Se crea la lista del spinner con 4 valores de ejemplo, también usamos el método getHourOfDay() para conseguir la hora del día y verificar si es de día o de noche.

Y por último controlamos la visibilidad de la barra de progreso con signUp(), el cual está ligado al botón.

<Button
            android:id="@+id/sign_up_button"
            android:onClick="@{handler::signUp}"/>

Al presionar el botón se intercambian las visibilidades.

Ejemplo android:visibility con Binding Converter

Descargar Código

Suscríbete y obtén el código gratis.

[sociallocker id=»7121″]Descargar Gratis[/sociallocker]

Ú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!