Adattatori associazione

Gli adattatori di associazione sono responsabili dell'esecuzione delle chiamate al framework appropriate per impostare i valori. Un esempio è l'impostazione di un valore di proprietà, come la chiamata del metodo setText(). Un altro esempio è l'impostazione di un listener di eventi, come la chiamata del metodo setOnClickListener().

La libreria di associazione dei dati consente di specificare il metodo chiamato per impostare un valore, fornire la tua logica di associazione e specificare il tipo di oggetto restituito utilizzando gli adattatori.

Impostare i valori degli attributi

Ogni volta che un valore associato cambia, la classe di associazione generata deve chiamare un metodo setter sulla vista con l'espressione di associazione. Puoi consentire alla libreria di associazione dati di determinare automaticamente il metodo oppure puoi dichiarare esplicitamente il metodo o fornire una logica personalizzata per selezionarne uno.

Selezione automatica del metodo

Per un attributo denominato example, la libreria trova automaticamente il metodo setExample(arg) che accetta tipi compatibili come argomento. Lo spazio dei nomi dell'attributo non viene preso in considerazione. Quando cerchi un metodo, vengono utilizzati solo il nome e il tipo di attributo.

Ad esempio, data l'espressione android:text="@{user.name}", la libreria cerca un metodo setText(arg) che accetti il tipo restituito da user.getName(). Se il tipo restituito di user.getName() è String, la libreria cerca un metodo setText() che accetti un argomento String. Se l'espressione restituisce un int, la libreria cerca un metodo setText() che accetti un argomento int. L'espressione deve restituire il tipo corretto. Se necessario, puoi trasmettere il valore restituito.

L'associazione di dati funziona anche se non esiste nessun attributo con il nome specificato. Puoi creare attributi per qualsiasi setter utilizzando l'associazione di dati. Ad esempio, la classe di assistenza DrawerLayout non ha attributi, ma ha molti setter. Il seguente layout utilizza automaticamente i metodi setScrimColor(int) e addDrawerListener(DrawerListener) come setter per gli attributi app:scrimColor e app:drawerListener rispettivamente:

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

Specifica un nome di metodo personalizzato

Alcuni attributi hanno setter che non corrispondono in base al nome. In questi casi, è possibile associare un attributo al setter utilizzando l'annotazione BindingMethods. L'annotazione viene utilizzata con una classe e può contenere più annotazioni BindingMethod, una per ogni metodo rinominato. I metodi di associazione sono annotazioni che puoi aggiungere a qualsiasi classe dell'app.

Nell'esempio seguente, l'attributo android:tint è associato al metodo setImageTintList(ColorStateList) e non al metodo 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"),
})

In genere, non è necessario rinominare i setter nelle classi di framework Android. Gli attributi sono già implementati utilizzando la convenzione dei nomi per trovare automaticamente i metodi di corrispondenza.

Fornisci una logica personalizzata

Alcuni attributi richiedono una logica di associazione personalizzata. Ad esempio, non esiste un setter associato per l'attributo android:paddingLeft. Viene invece fornito il metodo setPadding(left, top, right, bottom). Un metodo dell'adattatore di associazione statico con l'annotazione BindingAdapter ti consente di personalizzare il modo in cui viene chiamato un setter per un attributo.

Gli attributi delle classi di framework Android hanno già BindingAdapter annotazioni. L'esempio seguente mostra l'adattatore di associazione per l'attributo 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());
}

I tipi di parametri sono importanti. Il primo parametro determina il tipo di vista associato all'attributo. Il secondo parametro determina il tipo accettato nell'espressione di associazione dell'attributo specificato.

Gli adattatori di associazione sono utili anche per altri tipi di personalizzazione. Ad esempio, un caricatore personalizzato può essere chiamato da un thread di lavoro per caricare un'immagine.

Puoi anche avere adattatori che ricevono più attributi, come mostrato nell'esempio seguente:

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

Puoi utilizzare l'adattatore nel layout, come mostrato nell'esempio seguente. Tieni presente che @drawable/venueError si riferisce a una risorsa nella tua app. Se la risorsa viene racchiusa tra @{}, diventa un'espressione di associazione valida.

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

L'adattatore viene chiamato se imageUrl e error sono utilizzati per un oggetto ImageView, imageUrl è una stringa e error è Drawable. Se vuoi che l'adattatore venga chiamato quando sono impostati uno qualsiasi degli attributi, imposta il flag facoltativo requireAll dell'adattatore su false, come mostrato nell'esempio seguente:

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

I metodi degli adattatori di associazione possono assumere i vecchi valori nei relativi gestori. Un metodo che acquisisce valori vecchi e nuovi deve prima dichiarare tutti i valori precedenti per gli attributi, seguiti dai nuovi valori, come mostrato nell'esempio seguente:

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

I gestori di eventi possono essere utilizzati solo con interfacce o classi astratte con un metodo astratto, come mostrato nell'esempio seguente:

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

Utilizza questo gestore di eventi nel layout nel seguente modo:

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

Quando un listener ha più metodi, deve essere suddiviso in più listener. Ad esempio, View.OnAttachStateChangeListener ha due metodi: onViewAttachedToWindow(View) e onViewDetachedFromWindow(View). La libreria fornisce due interfacce per distinguere i relativi attributi e gestori:

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

Poiché la modifica di un listener può influire sull'altro, è necessario un adattatore che funzioni per uno degli attributi o per entrambi. Puoi impostare requireAll su false nell'annotazione per specificare che non a ogni attributo deve essere assegnata un'espressione di associazione, come mostrato nell'esempio seguente:

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'esempio precedente è leggermente complicato perché la classe View utilizza i metodi addOnAttachStateChangeListener() e removeOnAttachStateChangeListener() anziché un metodo setter per OnAttachStateChangeListener. La classe android.databinding.adapters.ListenerUtil aiuta a tenere traccia di questi ascoltatori in modo che possano essere rimossi nell'adattatore di associazione.

Conversioni di oggetti

Conversione automatica degli oggetti

Quando un elemento Object viene restituito da un'espressione di associazione, la libreria seleziona il metodo utilizzato per impostare il valore della proprietà. Object viene trasmesso a un tipo di parametro del metodo scelto. Questo comportamento è pratico nelle app che utilizzano la classe ObservableMap per archiviare i dati, come mostrato nell'esempio seguente:

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

L'oggetto userMap nell'espressione restituisce un valore che viene trasmesso automaticamente al tipo di parametro trovato nel metodo setText(CharSequence) utilizzato per impostare il valore dell'attributo android:text. Se il tipo di parametro è ambiguo, esegui il cast del tipo restituito nell'espressione.

Conversioni personalizzate

In alcuni casi, è necessaria una conversione personalizzata tra tipi specifici. Ad esempio, l'attributo android:background di una vista prevede un valore Drawable, ma il valore color specificato è un numero intero. L'esempio seguente mostra un attributo che prevede un Drawable, ma viene invece fornito un numero intero:

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

Ogni volta che è previsto un Drawable e viene restituito un numero intero, converti int in ColorDrawable. Per eseguire la conversione, utilizza un metodo statico con un'annotazione BindingConversion, come segue:

Kotlin

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

Java

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

Tuttavia, i tipi di valore forniti nell'espressione di associazione devono essere coerenti. Non puoi utilizzare tipi diversi nella stessa espressione, come mostrato nell'esempio seguente:

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

Risorse aggiuntive

Per saperne di più sull'associazione di dati, consulta le risorse seguenti.

Samples

Codelab

Post del blog