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?
- Usa el símbolo «
=
» en la expresión de binding. - Extiende de
BaseObservable
tu modelo. - Usa la anotación
@Bindable
sobre elget*()
de la propiedad relacionada. - 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:
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:
Clase | Atributo para two-way data binding |
---|---|
AdapterView | android:selectedItemPosition |
CalendarView | android:date |
CompoundButton | android:year |
NumberPicker | android:value |
RadioGroup | android:checkedButton |
RatingBar | android:rating |
SeekBar | android:progress |
TabHost | android:currentTab |
TextView | android:text |
TimePicker | android: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: