Adaptery do bindowania

Adaptery powiązań odpowiadają za wykonywanie odpowiednich wywołań platformy w celu ustawiania wartości. Możesz na przykład ustawić wartość właściwości, np. wywołać metodę setText(). Innym przykładem jest ustawianie detektora zdarzeń, np. wywołanie metody setOnClickListener().

Biblioteka powiązań danych umożliwia określenie metody wywoływanej w celu ustawienia wartości, podanie własnej logiki wiązania i określenie typu zwracanego obiektu za pomocą adapterów.

Ustawianie wartości atrybutów

Po każdej zmianie wartości granicznej wygenerowana klasa wiązania musi wywołać w widoku metodę ustawiającą z wyrażeniem wiążącym. Możesz pozwolić, aby biblioteka wiązań danych automatycznie określiła metodę, albo możesz ją zadeklarować jawnie lub podać własną logikę wyboru metody.

Automatyczny wybór metody

W przypadku atrybutu o nazwie example biblioteka automatycznie znajduje metodę setExample(arg), która jako argument akceptuje zgodne typy. Przestrzeń nazw atrybutu nie jest brana pod uwagę. Podczas wyszukiwania metody używane są tylko nazwa i typ atrybutu.

Na przykład w wyrażeniu android:text="@{user.name}" biblioteka szuka metody setText(arg), która akceptuje typ zwracany przez user.getName(). Jeśli zwracany typ elementu user.getName() to String, biblioteka szuka metody setText(), która akceptuje argument String. Jeśli wyrażenie zwraca wartość int, biblioteka wyszukuje metodę setText(), która akceptuje argument int. Wyrażenie musi zwracać prawidłowy typ. W razie potrzeby możesz rzutować wartość zwracaną.

Powiązanie danych działa nawet wtedy, gdy żaden atrybut o podanej nazwie nie istnieje. Za pomocą powiązania danych możesz tworzyć atrybuty dla dowolnego systemu ustawiającego. Na przykład klasa pomocy DrawerLayout nie ma atrybutów, ale ma dużo ustawiania. W tym układzie automatycznie używane są metody setScrimColor(int) i addDrawerListener(DrawerListener) jako metody ustawiające odpowiednio dla atrybutów app:scrimColor i app:drawerListener:

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

Podaj nazwę metody niestandardowej

Niektóre atrybuty mają ustawienia, które nie są zgodne pod względem nazwy. W takich sytuacjach atrybut można powiązać z metodą ustawiającą za pomocą adnotacji BindingMethods. Adnotacja jest używana w klasie i może zawierać wiele adnotacji BindingMethod, po jednej dla każdej metody ze zmienioną nazwą. Metody powiązania to adnotacje, które możesz dodawać do dowolnej klasy w aplikacji.

W tym przykładzie atrybut android:tint jest powiązany z metodą setImageTintList(ColorStateList), a nie z metodą 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"),
})

Zwykle nie musisz zmieniać nazw narzędzi ustawiających w klasach platformy Androida. Atrybuty są już zaimplementowane z wykorzystaniem konwencji nazw, która pozwala automatycznie znajdować metody dopasowania.

Wprowadź niestandardową logikę

Niektóre atrybuty wymagają niestandardowej logiki powiązania. Na przykład nie ma powiązanego parametru ustawiającego dla atrybutu android:paddingLeft. Zamiast tego używana jest metoda setPadding(left, top, right, bottom). Metoda statycznego wiązania wiązania z adnotacją BindingAdapter pozwala dostosować sposób wywoływania metody ustawiającej dla atrybutu.

Atrybuty klas platformy Androida mają już adnotacje BindingAdapter. Poniższy przykład pokazuje adapter wiązania dla atrybutu 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());
}

Typy parametrów są ważne. Pierwszy parametr określa typ widoku danych, który jest powiązany z atrybutem. Drugi parametr określa typ akceptowany w wyrażeniu powiązania dla danego atrybutu.

Adaptery do bindowania przydają się również do innych rodzajów dostosowywania. Na przykład niestandardowy program wczytywania może zostać wywołany z wątku instancji roboczej w celu wczytania obrazu.

Możesz też używać adapterów, które otrzymują wiele atrybutów, jak w tym przykładzie:

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

Możesz użyć przejściówki w układzie, jak pokazano w poniższym przykładzie. Pamiętaj, że @drawable/venueError odnosi się do zasobu w Twojej aplikacji. Otoczenie zasobu za pomocą @{} sprawia, że jest on prawidłowym wyrażeniem wiążącym.

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

Adapter jest wywoływany, jeśli obiekty imageUrl i error są używane w obiekcie ImageView, imageUrl to ciąg znaków, a error to Drawable. Jeśli chcesz, by adapter był wywoływany po ustawieniu któregokolwiek z atrybutów, ustaw opcjonalną flagę requireAll adaptera na false, jak w tym przykładzie:

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

Metody adaptera powiązania mogą przyjmować stare wartości z modułów obsługi. Metoda, która pobiera stare i nowe wartości, musi najpierw zadeklarować wszystkie stare wartości dla atrybutów, a po nich nowe wartości, jak w tym przykładzie:

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

Modułów obsługi zdarzeń można używać z interfejsami lub klasami abstrakcyjnymi tylko z jedną metodą abstrakcyjną, jak w tym przykładzie:

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

Użyj tego modułu obsługi zdarzeń w układzie w ten sposób:

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

Jeśli detektor ma wiele metod, należy go podzielić na większą liczbę detektorów. Na przykład w polu View.OnAttachStateChangeListener dostępne są 2 metody: onViewAttachedToWindow(View) i onViewDetachedFromWindow(View). Biblioteka udostępnia 2 interfejsy, które pozwalają rozróżniać atrybuty i moduły obsługi:

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

Ponieważ zmiana jednego nasłuchującego może wpływać na drugi, potrzebujesz adaptera, który będzie działać w przypadku jednego z tych atrybutów lub obu. W adnotacji możesz ustawić requireAll na false, aby określić, że nie do każdego atrybutu musi być przypisane wyrażenie powiązania, jak pokazano w poniższym przykładzie:

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

Powyższy przykład jest nieco skomplikowany, ponieważ klasa View używa metod addOnAttachStateChangeListener() i removeOnAttachStateChangeListener() zamiast metody ustawiającej dla OnAttachStateChangeListener. Klasa android.databinding.adapters.ListenerUtil pomaga śledzić te detektory i można je usunąć w adapterze wiązania.

Konwersje obiektów

Automatyczna konwersja obiektów

Gdy z wyrażenia powiązania zwracane jest Object, biblioteka wybiera metodę używaną do ustawiania wartości właściwości. Element Object jest rzutowany na typ parametru wybranej metody. Jest to wygodne w przypadku aplikacji używających klasy ObservableMap do przechowywania danych. W tym przykładzie:

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

Obiekt userMap w wyrażeniu zwraca wartość, która jest automatycznie rzutowana na typ parametru znaleziony w metodzie setText(CharSequence) używanej do ustawiania wartości atrybutu android:text. Jeśli typ parametru jest niejednoznaczny, rzutuj zwracany typ w wyrażeniu.

Konwersje niestandardowe

W niektórych sytuacjach między określonymi typami wymagana jest konwersja niestandardowa. Na przykład atrybut android:background widoku oczekuje wartości Drawable, a podana wartość color jest liczbą całkowitą. Ten przykład pokazuje atrybut, który oczekuje wartości Drawable, ale zamiast niej podana jest liczba całkowita:

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

Gdy oczekiwana jest wartość Drawable i zwracana jest liczba całkowita, przekonwertuj int na ColorDrawable. Aby dokonać konwersji, użyj metody statycznej z adnotacją BindingConversion w ten sposób:

Kotlin

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

Java

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

Jednak typy wartości podane w wyrażeniu powiązania muszą być spójne. W tym samym wyrażeniu nie można używać różnych typów, jak widać w tym przykładzie:

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

Dodatkowe materiały

Więcej informacji o wiązaniach danych znajdziesz w tych materiałach:

Próbki

Ćwiczenia z programowania

Posty na blogu