Adaptadores de vinculación

Los adaptadores de vinculación son responsables de realizar las llamadas de marco de trabajo apropiadas para establecer valores. Un ejemplo es establecer 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 mostrado mediante adaptadores.

Cómo establecer valores de atributo

Cada vez que un valor vinculado cambia, la clase de vinculación generada debe invocar un método establecedor 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, declare de manera explícita el método o proporcione una lógica personalizada para seleccionar un método.

Selección automática de métodos

Para un atributo llamado example, la biblioteca intenta automáticamente encontrar el método setExample(arg) que acepta tipos compatibles como el argumento. El espacio de nombres del atributo no se considera, solo se utilizan el nombre y el tipo de atributo cuando se busca un método.

Por ejemplo, dada la expresión android:text="@{user.name}", la biblioteca busca un método setText(arg) que acepte el tipo mostrado por 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 devuelve un int en su lugar, la biblioteca busca un método setText() que acepte un argumento int. La expresión debe mostrar el tipo correcto; puedes transmitir el valor de resultado si es necesario.

La vinculación de datos funciona incluso si no existe un atributo con el nombre dado. Luego, puedes crear atributos para cualquier establecedor mediante vinculaciones de datos. Por ejemplo, la clase de compatibilidad DrawerLayout no tiene ningún atributo, pero sí muchos establecedores. En el siguiente diseño, se utilizan de forma automática los setScrimColor(int) y setDrawerListener(DrawerListener) como el establecedor para los atributos app:scrimColor y app:drawerListener, respectivamente:

<android.support.v4.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 establecedores que no coinciden por nombre. En ese caso, un atributo puede asociarse con el establecedor utilizando la anotación BindingMethods. La anotación se usa con una clase y puede contener múltiples anotaciones de BindingMethod, una para cada método renombrado. Los métodos de vinculación son anotaciones que se pueden agregar a cualquier clase en tu app. En el siguiente ejemplo, el atributo android:tint está asociado con el 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"),
    })

    

La mayoría de las veces no es necesario cambiar el nombre de los establecedores en las clases de marco de trabajo de Android. Los atributos ya se implementaron utilizando la convención de nombres para buscar automáticamente métodos coincidentes.

Proporciona lógica personalizada

Algunos atributos requieren una lógica de vinculación personalizada. Por ejemplo, no hay un establecedor 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 permite personalizar el nombre de un establecedor para un atributo.

Los atributos de las clases de marco de trabajo de Android ya tienen anotaciones BindingAdapter. Por ejemplo, a continuación 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 para el atributo dado.

Los adaptadores de vinculació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.

Los adaptadores de vinculación que defines anulan los adaptadores predeterminados proporcionados por el marco de trabajo de Android cuando hay un conflicto.

También puede haber 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 en tu app. Delimitar el recurso con @{} hace que sea una expresión de vinculación válida.

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

El adaptador se llama si tanto imageUrl como el error se usan para un objeto ImageView y la imageUrl es una string y el error es un Drawable. Si deseas que se llame al adaptador cuando se establezca alguno de los atributos, puedes configurar 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 de adaptador de vinculación pueden tomar opcionalmente 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(padding,
                        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 que tienen:

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 también puede afectar al otro, necesitas un adaptador que funcione para cada atributo o para ambos. Puedes establecer requireAll en false en la anotación para especificar que no se debe asignar a todos los atributos una expresión de vinculación, 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 más complicado de lo normal porque la clase View usa los métodos addOnAttachStateChangeListener() y removeOnAttachStateChangeListener() en lugar de un método establecedor para OnAttachStateChangeListener. La clase android.databinding.adapters.ListenerUtil ayuda a realizar un seguimiento de los objetos de escucha anteriores para que puedan quitarse en el adaptador de vinculación.

Cuando se anotan las interfaces OnViewDetachedFromWindow y OnViewAttachedToWindow con @TargetApi(VERSION_CODES.HONEYCOMB_MR1), el generador de código de vinculación de datos sabe que solo se debe generar el objeto de escucha cuando se ejecuta en Android 3.1 (API nivel 12) y versiones posteriores, la misma versión compatible con el método addOnAttachStateChangeListener().

Conversiones de objetos

Conversión automática de objetos

Cuando se devuelve un Object de una expresión de vinculación, la biblioteca elige el método utilizado 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 en la expresión muestra un valor, el cual se transmite 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, debes transmitir 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 de color especificado es un número entero. En el siguiente ejemplo, se muestra un atributo que espera un Drawable, pero, en su lugar, se proporciona un número entero:

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

Cuando se espera un Drawable y se muestra un número entero, el int debe convertirse en un ColorDrawable. La conversión se puede realizar usando 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:

<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 adicionales.

Muestras

Codelabs

Entradas de blog (en inglés)