Adaptadores de vinculação

Os adaptadores de vinculação são responsáveis por fazer as chamadas de framework adequadas para definir valores. Um exemplo é definir um valor de propriedade, como chamar o método setText(). Outro exemplo é definir um listener de eventos, como chamar o método setOnClickListener().

A biblioteca Data Binding permite especificar o método chamado para definir um valor, fornecer sua própria lógica de vinculação e especificar o tipo de objeto retornado usando adaptadores.

Definir valores de atributos

Sempre que um valor vinculado muda, a classe de vinculação gerada precisa chamar um método setter na visualização com a expressão de vinculação. Você pode permitir que a biblioteca Data Binding defina automaticamente o método ou declarar explicitamente o método ou fornecer uma lógica personalizada para selecionar um método.

Seleção automática de método

Para um atributo chamado example, a biblioteca encontra automaticamente o método setExample(arg), que aceita tipos compatíveis como argumento. O namespace do atributo não é considerado. Apenas o nome e o tipo do atributo são usados ao pesquisar um método.

Por exemplo, considerando a expressão android:text="@{user.name}", a biblioteca procura um método setText(arg) que aceite o tipo retornado por user.getName(). Se o tipo de retorno de user.getName() for String, a biblioteca vai procurar um método setText() que aceite um argumento String. Se a expressão retornar um int, a biblioteca vai procurar um método setText() que aceite um argumento int. A expressão precisa retornar o tipo correto. É possível transmitir o valor de retorno, se necessário.

A vinculação de dados funciona mesmo que não exista atributo com o nome específico. É possível criar atributos para qualquer setter usando a vinculação de dados. Por exemplo, a classe de suporte DrawerLayout não tem atributos, mas tem muitos setters. O layout abaixo usa automaticamente os métodos setScrimColor(int) e addDrawerListener(DrawerListener) como setter dos atributos app:scrimColor e 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}">

Especificar um nome de método personalizado

Alguns atributos têm setters que não correspondem ao nome. Nessas situações, um atributo pode ser associado ao setter usando a anotação BindingMethods. A anotação é usada com uma classe e pode conter várias anotações BindingMethod, uma para cada método renomeado. Métodos de vinculação são anotações que você pode adicionar a qualquer classe do seu app.

No exemplo a seguir, o atributo android:tint está associado ao método setImageTintList(ColorStateList), não ao 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"),
})

Normalmente, não é necessário renomear setters em classes de framework do Android. Os atributos já foram implementados usando a convenção de nomes para encontrar automaticamente os métodos correspondentes.

Fornecer uma lógica personalizada

Alguns atributos precisam de uma lógica de vinculação personalizada. Por exemplo, não há setter associado para o atributo android:paddingLeft. Em vez disso, o método setPadding(left, top, right, bottom) é fornecido. Um método de adaptador de vinculação estático com a anotação BindingAdapter permite personalizar a forma como o setter de um atributo é chamado.

Os atributos das classes do framework do Android já têm anotações BindingAdapter. O exemplo a seguir mostra o adaptador de vinculação para o 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());
}

Os tipos de parâmetro são importantes. O primeiro parâmetro determina o tipo de visualização associada ao atributo. O segundo parâmetro determina o tipo aceito na expressão de vinculação para o atributo determinado.

Os adaptadores de vinculação também são úteis para outros tipos de personalização. Por exemplo, um carregador personalizado pode ser chamado a partir de uma linha de execução de worker para carregar uma imagem.

Você também pode ter adaptadores que recebem vários atributos, conforme mostrado no exemplo a seguir:

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);
}

Você pode usar o adaptador no layout, conforme mostrado no exemplo abaixo. Observe que @drawable/venueError se refere a um recurso no seu app. Ao envolvê-lo com @{}, ele se torna uma expressão de vinculação válida.

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

O adaptador será chamado se imageUrl e error forem usados para um objeto ImageView, se imageUrl for uma string e se error for uma Drawable. Se você quiser que o adaptador seja chamado quando qualquer um dos atributos estiver definido, defina a flag requireAll opcional do adaptador como false, conforme mostrado no exemplo a seguir.

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);
  }
}

Os métodos do adaptador de vinculação podem usar os valores antigos nos gerenciadores. Um método que aceita valores antigos e novos precisa declarar primeiro todos os valores antigos dos atributos, seguidos pelos novos valores, como mostrado no exemplo abaixo:

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());
   }
}

Os manipuladores de eventos só podem ser usados com interfaces ou classes abstratas com um método abstrato, conforme mostrado no exemplo a seguir:

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);
    }
  }
}

Use esse manipulador de eventos no seu layout da seguinte forma:

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

Quando um listener tem vários métodos, ele precisa ser dividido em vários listeners. Por exemplo, View.OnAttachStateChangeListener tem dois métodos: onViewAttachedToWindow(View) e onViewDetachedFromWindow(View). A biblioteca fornece duas interfaces para diferenciar os atributos e gerenciadores deles:

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);
}

Como a mudança de um listener pode afetar o outro, você precisa de um adaptador que funcione para um dos atributos ou para ambos. Você pode definir requireAll como false na anotação para especificar que nem todos os atributos precisam receber uma expressão de vinculação, conforme mostrado no exemplo abaixo.

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);
        }
    }
}

O exemplo acima é um pouco complicado porque a classe View usa os métodos addOnAttachStateChangeListener() e removeOnAttachStateChangeListener() em vez de um método setter para OnAttachStateChangeListener. A classe android.databinding.adapters.ListenerUtil ajuda a monitorar esses listeners para que eles possam ser removidos no adaptador de vinculação.

Conversões de objeto

Conversão automática de objetos

Quando uma Object é retornada de uma expressão de vinculação, a biblioteca seleciona o método usado para definir o valor da propriedade. O Object é transmitido para um tipo de parâmetro do método escolhido. Esse comportamento é conveniente em apps que usam a classe ObservableMap para armazenar dados, conforme mostrado no exemplo abaixo.

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

O objeto userMap na expressão retorna um valor, que é transmitido automaticamente para o tipo de parâmetro encontrado no método setText(CharSequence) usado para definir o valor do atributo android:text. Se o tipo de parâmetro for ambíguo, transmita o tipo de retorno na expressão.

Conversões personalizadas

Em algumas situações, uma conversão personalizada é necessária entre tipos específicos. Por exemplo, o atributo android:background de uma visualização espera um Drawable, mas o valor de color especificado é um número inteiro. O exemplo abaixo mostra um atributo que espera um Drawable, mas um número inteiro é fornecido:

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

Sempre que um Drawable for esperado e um número inteiro for retornado, converta o int em um ColorDrawable. Para realizar a conversão, use um método estático com uma anotação BindingConversion, desta forma:

Kotlin

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

Java

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

No entanto, os tipos de valores fornecidos na expressão de vinculação precisam ser consistentes. Não é possível usar tipos diferentes na mesma expressão, conforme mostrado no exemplo a seguir:

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

Outros recursos

Para saber mais sobre a vinculação de dados, consulte os recursos a seguir.

Exemplos

Codelabs

Postagens do blog