Data Binding 3: Vinculación Two-way

Resumen: En este tutorial, aprenderás a usar la two-way data binding (vinculación de datos bidireccional) para que el modelo asigne valores a la vista y viceversa.

Recuerda leer las partes 1 y 2 para comprender mejor el tutorial:

¿Qué Es Two-way Data Binding?

Es un mecanismo para asignar el valor de un atributo a un view y la escucha que reacciona antes los cambios de dicho atributo.

La diferencia entre el binding one-way (la forma convencional) y el two-way es que el two-way además de permitir que la vista sea actualizada por el modelo, habilita a los eventos de la vista para que actualicen al modelo en una sola expresión.

¿Cómo hacerlo?

  1. Usa el símbolo «=» en la expresión de binding.
  2. Extiende de BaseObservable tu modelo.
  3. Usa la anotación @Bindable sobre el get*() de la propiedad relacionada.
  4. Llama a notifyPropertyChanged() en su método set para notificar cambios a la librería.

Ejemplo:

Marcamos el atributo para recibir vinculaciones de doble vía:

<TextView
    android:id="@+id/normal_text"
    android:text="@={viewmodel.textProperty}"/>

La clase del modelo es extendida de BaseObservable:

public class ExampleViewModel extends BaseObservable {
    private String textProperty;    
}

Generamos un accesor get para el atributo y le ponemos @Bindable:

@Bindable
public String getTextProperty() {
    return textProperty;
}

Llamamos la notificación dentro del método set del atributo:

public void setTextProperty(String textProperty) {
    if (!textProperty.equals(this.textProperty)) {
        this.textProperty = textProperty;
        notifyPropertyChanged(BR.textProperty);
    }
}

La clase BR es generada por la anotación @Bindable para contener los ids de las propiedades cuyos cambios serán notificados a la librería.

De esta forma, cuando el texto del TextView sea alterado tanto el atributo textProperty como el view normal_text contendrán el mismo valor.

Convertidores Para Data Binding Two-way

Usar una clase convertidora nace de la necesidad de ajustar el tipo del atributo del modelo al tipo de dato del view a mostrar.

Ejemplo: supón que el atributo dateProperty del modelo es de tipo Date y tu deseas mostrar su valor en android:text de un TextView.

Si intentas asignarlo sin más, Android Studio te mostrará esto:

Error al usar atributo two-way con tipo no compatible

Para solucionarlo creamos un método estático público que retorne el valor correspondiente. En este caso String:

public class ExampleConverter {

@InverseMethod("bToA")
public static String aToB(Date date) {
return date.toString();
}

public static Date bToA(String a) {
try {
return DateFormat.getDateInstance().parse(a);
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
}

Como ves, aToB() toma una fecha y la convierte en String.

Conversión bidireccional: usamos @InverseMethod para decirle a la librería que el método bToA() será el encargado de hacer el mapeado inverso. De esta forma la librería sabrá como asignar el valor al atributo del modelo.

Finalmente llamas al método en la expresión two-way:

<TextView
    android:id="@+id/date_text"
    android:text="@={ExampleConverter.aToB(viewmodel.dateProperty)}"/>

Atributos Personalizados

La librería de Data Binding da soporte de doble vinculación a varios views de uso cotidiano. La siguiente es una tabla de los atributos que ya vienen preparados para two-way:

ClaseAtributo para two-way data binding
AdapterViewandroid:selectedItemPosition
android:selection
CalendarViewandroid:date
CompoundButtonandroid:year
android:month
android:day
NumberPickerandroid:value
RadioGroupandroid:checkedButton
RatingBarandroid:rating
SeekBarandroid:progress
TabHostandroid:currentTab
TextViewandroid:text
TimePickerandroid:hour
android:minute

Si deseas personalizar atributos de tus propias clases o cambiar el flujo en que lo hacen las clases ya existentes, entonces sigue los pasos de esta sección.

Ejemplo Data Binding Two-way

Usaremos un formulario que representa la edición de un cliente con el fin de probar diferentes views y sus atributos bidireccionales.

Veamos como ligar el modelo con cada view.

EditTexts Con Two-way

Atributo XML

El atributo para two-way es android:text. Recuerda que EditText es una subclase de TextView.

Propiedad del modelo

El nombre, teléfono y dirección serán representados por name, phone y address respectivamente en la clase de ejemplo CustomerViewModel.

Recuerda que debe extender de BaseObservable.

public class CustomerViewModel extends BaseObservable {
    private String name;
    private String phone;
    private String address;
    private Gender gender;
    private boolean natural;
    private boolean subscriber;

    public CustomerViewModel(String name, String phone,
                             String address, Gender gender,
                             boolean natural, boolean subscriber) {
        this.name = name;
        this.phone = phone;
        this.address = address;
        this.gender = gender;
        this.natural = natural;
        this.subscriber = subscriber;
    }

    @Bindable
    public String getName() {
        return name;
    }


    public void setName(String name) {
        if (!this.name.equals(name)) {
            this.name = name;
            notifyPropertyChanged(BR.name);
        }
    }

    @Bindable
    public String getPhone() {
        return phone;
    }


    public void setPhone(String phone) {
        if (!this.phone.equals(phone)) {
            this.phone = phone;
            notifyPropertyChanged(BR.phone);
        }
    }

    @Bindable
    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        if (!this.address.equals(address)) {
            this.address = address;
        }
    }
}

Cada atributo del modelo contiene @Bindable en su get y la llamada de notifyPropertyChanged() en el set.

Asignar valores a android:text

Los 3 campos tan solo requieren una asignación sencilla, por lo que asignamos las propiedades de la clase CustomerViewModel correspondientes.

<data>  

    <variable
        name="viewmodel"
        type="com.herpro.databinding3.data.CustomerViewModel" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout ...>    

    <EditText
        android:id="@+id/name_field"
        android:text="@={viewmodel.name}" />

    <EditText
        android:id="@+id/phone_field"
        android:text="@={viewmodel.phone}" />

    <EditText
        android:id="@+id/address_field"
        android:text="@={viewmodel.address}" />
...
</androidx.constraintlayout.widget.ConstraintLayout>

Spinner Con Two-way

Atributo XML

Usa android:selection para two-way; Este recibe enteros que representan la posición del item actualmente seleccionado.

<Spinner
    android:id="@+id/gender_dropdown"
    android:entries="@array/genders"
    android:selection="@={0}" />

Propiedad del modelo

Si revisas el modelo verás que el género es presentado por la propeidad gender del tipo enumerable Gender. La idea es ver cómo usar una clase convertidora para setear el valor en ambas direcciones.

@Bindable
public Gender getGender() {
    return gender;
}

public void setGender(Gender gender) {
    if (this.gender != gender) {
        this.gender = gender;
        notifyPropertyChanged(BR.gender);
    }
}

public enum Gender {
    MASCULINE, FEMENINE
}

Convertir enum en int

Ya que la clase Spinner usa el atributo entero android:selection para el two-way, entonces crearemos los métodos de conversión basados en la existencia de dos valores (0 para masculino y 1 para femenino):

public class Converters {

    @InverseMethod("intToGender")
    public static int genderToInt(Gender gender) {
        return gender == Gender.MASCULINE ? 0 : 1;
    }

    public static Gender intToGender(int gender) {
        return gender == 0 ? Gender.MASCULINE : Gender.FEMENINE;
    }
}

Asignar valor a android:selection

Con lo que hacemos la conversión en la expresión del atributo android:selection:

<data>

    <import type="com.herpro.databinding3.ui.Converters" />

</data>

<androidx.constraintlayout.widget.ConstraintLayout>
    ...
    <Spinner
        android:id="@+id/gender_dropdown"
        android:entries="@array/genders"
        android:selection="@={Converters.genderToInt(viewmodel.gender)}" />
...

RadioGroup Con Two-way

Atributo XML

En la tabla de la sección anterior vimos que seleccionar el RadioButton del grupo se realiza con android:checkedButton que a su vez recibe binding bidireccional. Este recibe el ID del radio por marcar.

<RadioGroup
    android:id="@+id/customer_type_group"
    android:checkedButton="@={R.id.r1}">

Propiedad del modelo

La propiedad del modelo que lo representa es el booleano natural.

@Bindable
public boolean isNatural() {
return natural;
}

public void setNatural(boolean natural) {
if (this.natural != natural) {
this.natural = natural;
notifyPropertyChanged(BR.natural);
}
}

Converter de boolean a int

La función del grupo de radios en este ejemplo es determinar si el cliente es de tipo empresarial o natural.

Si natural es true, entonces asignamos el radio R.id.r2, de lo contrario será R.id.r1; Esta lógica hará la conversión necesaria para asignar el radio seleccionado:

@InverseMethod("intToBoolean")
public static int booleanToInt(boolean natural) {
return natural ? R.id.r2 : R.id.r1;
}

public static boolean intToBoolean(int radioId) {
return radioId == R.id.r2;
}

Asignación del valor a checkedButton

Concluyendo, asignaremos el valor a android:checkedButton convirtiendo la propiedad natural:

<RadioGroup
    android:id="@+id/customer_type_group"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="24dp"
    android:checkedButton="@={Converters.booleanToInt(viewmodel.natural)}"
    android:orientation="horizontal"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/gender_dropdown">

    <RadioButton
        android:id="@+id/r1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/padding_layout"
        android:layout_marginRight="@dimen/padding_layout"
        android:text="@string/enterprise_customer_option" />

    <RadioButton
        android:id="@+id/r2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/natural_customer_option" />
</RadioGroup>

CheckBox Con Two-way

Atributo XML

Usaremos android:checked para la vinculación del CheckBox. Este permite valores booleanos.

<CheckBox
    android:id="@+id/c1"
    android:checked="@={true}"/>

Propiedad del modelo

La variable booleana subscriber determinará si el cliente está suscrito al canal de emails del servicio.

@Bindable
public boolean isSubscriber() {
return subscriber;
}

public void setSubscriber(boolean subscriber) {
if (this.subscriber != subscriber) {
this.subscriber = subscriber;
notifyPropertyChanged(BR.subscriber);
}
}

Asignación del valor a android:checked

Ahora simplemente asignaremos directamente la propiedad desde la variable de binding:

<CheckBox
    android:id="@+id/c1"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="24dp"
    android:checked="@={viewmodel.subscriber}"
    android:text="@string/is_subscriber_field"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/customer_type_group" />

Para finalizar, comprobaremos la población de los views desde el objeto de binding en la actividad principal:

public class MainActivity extends AppCompatActivity {

private CustomerViewModel mViewModel;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(
this,
R.layout.activity_main
);

mViewModel = new CustomerViewModel(
"James",
"3216574",
"Cra 77 #2",
Gender.MASCULINE,
true,
false
);
CustomerViewModel viewmodel = mViewModel;
binding.setViewmodel(viewmodel);
binding.setHandler(this);
}

}

Adicionalmente comprobaremos si el two-way ha disparado las escuchas de los views para actualizar las propiedades del modelo. Para ello añadimos un método para el botón de guardado:

public void saveCustomer(View v) {
    String sb = "Cliente actual:(n" + "Nombre: " + mViewModel.getName() + "n" +
            "Teléfono: " + mViewModel.getPhone() + "n" +
            "Dirección: " + mViewModel.getAddress() + "n" +
            "Género: " + mViewModel.getGender() + "n" +
            "Tipo de cliente: " + (mViewModel.isNatural()?"Natural":"Empresarial") + "n" +
            "¿En el boletín?: " + mViewModel.isSubscriber() + "n)";
    Toast.makeText(this, sb, Toast.LENGTH_LONG).show();
}

No olvides el binding por referencia en el layout:

<?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">

    <data>

        ...
        <variable
            name="handler"
            type="com.herpro.databinding3.ui.MainActivity" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/padding_layout"
        tools:context=".ui.MainActivity">

        ...

        <Button
            android:id="@+id/save_button"
            android:onClick="@{handler::saveCustomer}" />

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

Descargar Código

Descarga el proyecto completo en Android Studio desde el siguiente enlace:

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