Adaptateurs de liaison

Les adaptateurs de liaison sont chargés d'effectuer les appels de framework appropriés pour définir des valeurs. Par exemple, vous pouvez définir une valeur de propriété, ce qui peut être l'appel de la méthode setText(). Un autre exemple consiste à définir un écouteur d'événements, comme l'appel de la méthode setOnClickListener().

La bibliothèque Data Binding vous permet de spécifier la méthode appelée pour définir une valeur, de fournir votre propre logique de liaison et de spécifier le type de l'objet renvoyé à l'aide d'adaptateurs.

Définir les valeurs des attributs

Chaque fois qu'une valeur liée change, la classe de liaison générée doit appeler une méthode "setter" sur la vue avec l'expression de liaison. Vous pouvez laisser la bibliothèque de liaison de données déterminer automatiquement la méthode, ou la déclarer explicitement ou fournir une logique personnalisée pour en sélectionner une.

Sélection automatique de la méthode

Pour un attribut nommé example, la bibliothèque trouve automatiquement la méthode setExample(arg) qui accepte les types compatibles comme argument. L'espace de noms de l'attribut n'est pas pris en compte. Seuls le nom et le type d'attribut sont utilisés lors de la recherche d'une méthode.

Par exemple, pour l'expression android:text="@{user.name}", la bibliothèque recherche une méthode setText(arg) qui accepte le type renvoyé par user.getName(). Si le type renvoyé de user.getName() est String, la bibliothèque recherche une méthode setText() qui accepte un argument String. Si l'expression renvoie un int, la bibliothèque recherche une méthode setText() qui accepte un argument int. L'expression doit renvoyer le type correct. Vous pouvez convertir la valeur renvoyée si nécessaire.

La liaison de données fonctionne même s'il n'existe aucun attribut avec le nom donné. Vous pouvez créer des attributs pour n'importe quel setter à l'aide de la liaison de données. Par exemple, la classe de support DrawerLayout n'a pas d'attributs, mais de nombreux setters. La mise en page suivante utilise automatiquement les méthodes setScrimColor(int) et addDrawerListener(DrawerListener) comme setter pour les attributs app:scrimColor et app:drawerListener, respectivement:

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

Spécifier un nom de méthode personnalisé

Certains attributs ont des setters qui ne correspondent pas par leur nom. Dans ces situations, un attribut peut être associé au setter à l'aide de l'annotation BindingMethods. L'annotation est utilisée avec une classe et peut contenir plusieurs annotations BindingMethod, une pour chaque méthode renommée. Les méthodes de liaison sont des annotations que vous pouvez ajouter à n'importe quelle classe de votre application.

Dans l'exemple suivant, l'attribut android:tint est associé à la méthode setImageTintList(ColorStateList), et non à la méthode 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"),
})

En règle générale, il n'est pas nécessaire de renommer les setters dans les classes du framework Android. Les attributs sont déjà implémentés à l'aide de la convention de noms pour rechercher automatiquement les méthodes de correspondance.

Fournir une logique personnalisée

Certains attributs nécessitent une logique de liaison personnalisée. Par exemple, il n'existe pas de setter associé à l'attribut android:paddingLeft. À la place, la méthode setPadding(left, top, right, bottom) est fournie. Une méthode d'adaptateur de liaison statique avec l'annotation BindingAdapter vous permet de personnaliser la façon dont un setter est appelé pour un attribut.

Les attributs des classes du framework Android comportent déjà des annotations BindingAdapter. L'exemple suivant montre l'adaptateur de liaison pour l'attribut 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());
}

Les types de paramètres sont importants. Le premier paramètre détermine le type de vue associé à l'attribut. Le deuxième paramètre détermine le type accepté dans l'expression de liaison pour l'attribut donné.

Les adaptateurs de liaison sont également utiles pour d'autres types de personnalisation. Par exemple, un chargeur personnalisé peut être appelé à partir d'un thread de nœud de calcul pour charger une image.

Vous pouvez également avoir des adaptateurs qui reçoivent plusieurs attributs, comme illustré dans l'exemple suivant:

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

Vous pouvez utiliser l'adaptateur dans votre mise en page, comme illustré dans l'exemple suivant. Notez que @drawable/venueError fait référence à une ressource de votre application. Le fait d'entourer la ressource de @{} en fait une expression de liaison valide.

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

L'adaptateur est appelé si imageUrl et error sont utilisés pour un objet ImageView, imageUrl est une chaîne et error est un Drawable. Si vous souhaitez que l'adaptateur soit appelé lorsque l'un des attributs est défini, définissez l'indicateur facultatif requireAll de l'adaptateur sur false, comme illustré dans l'exemple suivant:

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

Les méthodes d'adaptateur de liaison peuvent utiliser les anciennes valeurs dans leurs gestionnaires. Une méthode qui utilise des valeurs anciennes et nouvelles doit d'abord déclarer toutes les anciennes valeurs pour les attributs, puis les nouvelles, comme illustré dans l'exemple suivant:

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

Les gestionnaires d'événements ne peuvent être utilisés qu'avec des interfaces ou des classes abstraites avec une seule méthode abstraite, comme illustré dans l'exemple suivant:

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

Utilisez ce gestionnaire d'événements dans votre mise en page comme suit:

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

Lorsqu'un écouteur comporte plusieurs méthodes, il doit être divisé en plusieurs écouteurs. Par exemple, View.OnAttachStateChangeListener comporte deux méthodes : onViewAttachedToWindow(View) et onViewDetachedFromWindow(View). La bibliothèque fournit deux interfaces pour différencier leurs attributs et leurs gestionnaires:

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

Étant donné que la modification d'un écouteur peut affecter l'autre, vous avez besoin d'un adaptateur qui fonctionne pour l'un ou l'autre des attributs. Vous pouvez définir requireAll sur false dans l'annotation pour spécifier qu'une expression de liaison ne doit pas être attribuée à tous les attributs, comme illustré dans l'exemple suivant:

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

L'exemple ci-dessus est légèrement compliqué, car la classe View utilise les méthodes addOnAttachStateChangeListener() et removeOnAttachStateChangeListener() au lieu d'une méthode setter pour OnAttachStateChangeListener. La classe android.databinding.adapters.ListenerUtil permet d'effectuer le suivi de ces écouteurs afin qu'ils puissent être supprimés dans l'adaptateur de liaison.

Conversions d'objets

Conversion automatique d'objets

Lorsqu'un objet Object est renvoyé à partir d'une expression de liaison, la bibliothèque sélectionne la méthode utilisée pour définir la valeur de la propriété. Object est converti en un type de paramètre de la méthode choisie. Ce comportement est pratique dans les applications qui utilisent la classe ObservableMap pour stocker des données, comme illustré dans l'exemple suivant:

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

L'objet userMap de l'expression renvoie une valeur, qui est automatiquement convertie en type de paramètre trouvé dans la méthode setText(CharSequence) utilisée pour définir la valeur de l'attribut android:text. Si le type de paramètre est ambigu, convertissez le type renvoyé dans l'expression.

Conversions personnalisées

Dans certains cas, une conversion personnalisée est requise pour des types spécifiques. Par exemple, l'attribut android:background d'une vue attend une valeur Drawable, mais la valeur color spécifiée est un entier. L'exemple suivant montre un attribut qui attend une valeur Drawable, mais un entier est fourni à la place:

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

Chaque fois qu'une valeur Drawable est attendue et qu'un nombre entier est renvoyé, convertissez l'int en ColorDrawable. Pour effectuer la conversion, utilisez une méthode statique avec une annotation BindingConversion, comme suit:

Kotlin

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

Java

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

Toutefois, les types de valeurs fournis dans l'expression de liaison doivent être cohérents. Vous ne pouvez pas utiliser différents types dans la même expression, comme illustré dans l'exemple suivant:

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

Ressources supplémentaires

Pour en savoir plus sur la liaison de données, consultez les ressources suivantes.

Exemples

Ateliers de programmation

Articles de blog