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.
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.
Descargar Código
Suscríbete y obtén el código gratis.