Adaptadores de vinculación

Los adaptadores de vinculación son responsables de realizar las llamadas de framework adecuadas para establecer valores. Un ejemplo es configurar un valor de propiedad, como llamar al método setText(). Otro ejemplo es configurar un objeto de escucha de eventos, como llamar al método setOnClickListener().

La biblioteca de vinculación de datos te permite especificar el método llamado para establecer un valor, proporcionar tu propia lógica de vinculación y especificar el tipo de objeto que se muestra mediante adaptadores.

Cómo establecer valores de atributos

Cada vez que cambia un valor vinculado, la clase de vinculación generada debe llamar a un método set en la vista con la expresión de vinculación. Puedes permitir que la biblioteca de vinculación de datos determine automáticamente el método, o bien puedes declararlo explícitamente o proporcionar una lógica personalizada para seleccionar un método.

Selección automática de métodos

Para un atributo llamado example, la biblioteca encuentra automáticamente el método setExample(arg) que acepta tipos compatibles como argumento. No se tiene en cuenta el espacio de nombres del atributo. Cuando se busca un método, solo se usan el nombre y el tipo de atributo.

Por ejemplo, con la expresión android:text="@{user.name}", la biblioteca busca un método setText(arg) que acepte el tipo que muestra user.getName(). Si el tipo de datos que se muestra de user.getName() es String, la biblioteca busca un método setText() que acepte un argumento String. Si la expresión muestra un int, la biblioteca busca un método setText() que acepte un argumento int. La expresión debe mostrar el tipo correcto. Si es necesario, puedes convertir el valor que se muestra.

La vinculación de datos funciona incluso si no existe un atributo con el nombre dado. Puedes crear atributos para cualquier método set mediante la vinculación de datos. Por ejemplo, la clase de compatibilidad DrawerLayout no tiene atributos, pero sí muchos métodos set. En el siguiente diseño, se usan automáticamente los métodos setScrimColor(int) y addDrawerListener(DrawerListener) como método set para los atributos app:scrimColor y app:drawerListener, respectivamente:

<androidx.drawerlayout.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}">

Especifica un nombre de método personalizado

Algunos atributos tienen métodos set que no coinciden por nombre. En esos casos, se puede asociar un atributo con el método set utilizando la anotación BindingMethods. La anotación se usa con una clase y puede contener varias anotaciones BindingMethod, una para cada método al que se le cambió el nombre. Los métodos de vinculación son anotaciones que puedes agregar a cualquier clase en tu app.

En el siguiente ejemplo, el atributo android:tint está asociado con el método setImageTintList(ColorStateList), no con el método setTint():

Kotlin

@BindingMethods(value = [
    BindingMethod(
        type = android.widget.ImageView::class,
        attribute = "android:tint",
        method = "setImageTintList")])

Java

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

Por lo general, no es necesario cambiar el nombre de los métodos set en las clases del framework de Android. Los atributos ya están implementados con la convención de nombres para encontrar automáticamente métodos coincidentes.

Proporciona lógica personalizada

Algunos atributos requieren una lógica de vinculación personalizada. Por ejemplo, no hay un método set asociado para el atributo android:paddingLeft. En su lugar, se proporciona el método setPadding(left, top, right, bottom). Un método de adaptador de vinculación estático con la anotación BindingAdapter te permite personalizar el nombre de un método set para un atributo.

Los atributos de las clases de framework de Android ya tienen anotaciones BindingAdapter. En el siguiente ejemplo, se muestra el adaptador de vinculación para el atributo paddingLeft:

Kotlin

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}

Java

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
  view.setPadding(padding,
                  view.getPaddingTop(),
                  view.getPaddingRight(),
                  view.getPaddingBottom());
}

Los tipos de parámetros son importantes. El primer parámetro determina el tipo de vista asociada con el atributo. El segundo parámetro determina el tipo aceptado en la expresión de vinculación del atributo dado.

Los adaptadores de vinculación también son útiles para otros tipos de personalización. Por ejemplo, se puede llamar a un cargador personalizado desde un subproceso de trabajo para cargar una imagen.

También puedes tener adaptadores que reciban varios atributos, como se muestra en el siguiente ejemplo:

Kotlin

@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}

Java

@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
  Picasso.get().load(url).error(error).into(view);
}

Puedes usar el adaptador en tu diseño, como se muestra en el siguiente ejemplo. Ten en cuenta que @drawable/venueError hace referencia a un recurso de tu app. Delimitar el recurso con @{} la convierte en una expresión de vinculación válida.

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

Se llama al adaptador si se usan imageUrl y error para un objeto ImageView, imageUrl es una string y error es un Drawable. Si quieres que se llame al adaptador cuando se configure cualquiera de los atributos, configura la marca opcional requireAll del adaptador en false, como se muestra en el siguiente ejemplo:

Kotlin

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
    if (url == null) {
        imageView.setImageDrawable(placeholder);
    } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
    }
}

Java

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
  if (url == null) {
    imageView.setImageDrawable(placeholder);
  } else {
    MyImageLoader.loadInto(imageView, url, placeholder);
  }
}

Los métodos del adaptador de vinculación pueden tomar los valores anteriores en sus controladores. Un método que toma valores antiguos y nuevos debe declarar todos los valores anteriores para los atributos primero, seguidos de los valores nuevos, como se muestra en el siguiente ejemplo:

Kotlin

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
    if (oldPadding != newPadding) {
        view.setPadding(newPadding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }
}

Java

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
  if (oldPadding != newPadding) {
      view.setPadding(newPadding,
                      view.getPaddingTop(),
                      view.getPaddingRight(),
                      view.getPaddingBottom());
   }
}

Los controladores de eventos solo se pueden usar con interfaces o clases abstractas con un método abstracto, como se muestra en el siguiente ejemplo:

Kotlin

@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
        view: View,
        oldValue: View.OnLayoutChangeListener?,
        newValue: View.OnLayoutChangeListener?
) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue)
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue)
        }
    }
}

Java

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    if (oldValue != null) {
      view.removeOnLayoutChangeListener(oldValue);
    }
    if (newValue != null) {
      view.addOnLayoutChangeListener(newValue);
    }
  }
}

Usa este controlador de eventos en el diseño de la siguiente manera:

<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

Cuando un objeto de escucha tiene varios métodos, debe dividirse en múltiples objetos de escucha. Por ejemplo, View.OnAttachStateChangeListener tiene dos métodos: onViewAttachedToWindow(View) y onViewDetachedFromWindow(View). La biblioteca proporciona dos interfaces para diferenciar los atributos y controladores para ellos:

Kotlin

// Translation from provided interfaces in Java:
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewDetachedFromWindow {
    fun onViewDetachedFromWindow(v: View)
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewAttachedToWindow {
    fun onViewAttachedToWindow(v: View)
}

Java

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
  void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
  void onViewAttachedToWindow(View v);
}

Debido a que cambiar un objeto de escucha puede afectar al otro, necesitas un adaptador que funcione para cualquiera de los atributos o ambos. Puedes configurar requireAll como false en la anotación para especificar que no se debe asignar una expresión de vinculación a todos los atributos, como se muestra en el siguiente ejemplo:

Kotlin

@BindingAdapter(
        "android:onViewDetachedFromWindow",
        "android:onViewAttachedToWindow",
        requireAll = false
)
fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach: OnViewAttachedToWindow?) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
        val newListener: View.OnAttachStateChangeListener?
        newListener = if (detach == null && attach == null) {
            null
        } else {
            object : View.OnAttachStateChangeListener {
                override fun onViewAttachedToWindow(v: View) {
                    attach.onViewAttachedToWindow(v)
                }

                override fun onViewDetachedFromWindow(v: View) {
                    detach.onViewDetachedFromWindow(v)
                }
            }
        }

        val oldListener: View.OnAttachStateChangeListener? =
                ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener)
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener)
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener)
        }
    }
}

Java

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }
                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }

        OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
                R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

El ejemplo anterior es un poco complicado porque la clase View usa los métodos addOnAttachStateChangeListener() y removeOnAttachStateChangeListener() en lugar de un método set para OnAttachStateChangeListener. La clase android.databinding.adapters.ListenerUtil ayuda a realizar un seguimiento de estos objetos de escucha para que se puedan quitar en el adaptador de vinculación.

Conversiones de objetos

Conversión automática de objetos

Cuando se muestra un Object desde una expresión de vinculación, la biblioteca selecciona el método usado para establecer el valor de la propiedad. El Object se transmite a un tipo de parámetro del método elegido. Este comportamiento es conveniente en apps que usan la clase ObservableMap para almacenar datos, como se muestra en el siguiente ejemplo:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />

El objeto userMap de la expresión muestra un valor, que se convierte automáticamente al tipo de parámetro encontrado en el método setText(CharSequence) que se usa para establecer el valor del atributo android:text. Si el tipo de parámetro es ambiguo, convierte el tipo de datos que se muestra en la expresión.

Conversiones personalizadas

En algunos casos, se requiere una conversión personalizada entre tipos específicos. Por ejemplo, el atributo android:background de una vista espera un Drawable, pero el valor color especificado es un número entero. En el siguiente ejemplo, se muestra un atributo que espera un Drawable, pero se proporciona un número entero en su lugar:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Cada vez que se espera un Drawable y se muestra un número entero, convierte int en ColorDrawable. Para realizar la conversión, usa un método estático con una anotación BindingConversion, de la siguiente manera:

Kotlin

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

Java

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}

Sin embargo, los tipos de valor proporcionados en la expresión de vinculación deben ser coherentes. No puedes usar tipos diferentes en la misma expresión, como se muestra en el siguiente ejemplo:

// The @drawable and @color represent different value types in the same
// expression, which causes a build error.
<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Recursos adicionales

Para obtener más información sobre la vinculación de datos, consulta los siguientes recursos.

Ejemplos

Codelabs

Entradas de blog