Адаптеры привязки

Адаптеры связывания отвечают за выполнение соответствующих вызовов фреймворка для установки значений. Например, можно задать значение свойства, вызвав метод setText(). Другой пример – настройка прослушивателя событий, например вызов метода setOnClickListener().

Библиотека привязки данных позволяет указать метод, вызываемый для установки значения, предоставить собственную логику привязки и указать тип возвращаемого объекта с помощью адаптеров.

Задайте значения атрибутов

При каждом изменении связанного значения сгенерированный класс привязки должен вызывать метод setter в представлении с выражением привязки. Вы можете разрешить библиотеке привязки данных автоматически определять метод, или вы можете явно объявить метод или предоставить пользовательскую логику для выбора метода.

Автоматический выбор метода

Для атрибута с именем example библиотека автоматически находит метод setExample(arg), который принимает совместимые типы в качестве аргумента. Пространство имен атрибута не учитывается. При поиске метода используются только название и тип атрибута.

Например, для выражения android:text="@{user.name}" библиотека ищет метод setText(arg), который принимает тип, возвращаемый user.getName(). Если тип возвращаемого значения user.getName() – String, библиотека ищет метод setText(), который принимает аргумент String. Если выражение возвращает int, библиотека ищет метод setText(), который принимает аргумент int. Выражение должно возвращать правильный тип. При необходимости вы можете выполнить приведение полученного значения.

Привязка данных работает, даже если атрибута с указанным названием не существует. Вы можете создавать атрибуты для любого сеттера, используя привязку данных. Например, у класса поддержки DrawerLayout нет атрибутов, но есть множество методов-сеттеров. В следующем макете методы setScrimColor(int) и addDrawerListener(DrawerListener) автоматически используются в качестве сеттеров для атрибутов app:scrimColor и app:drawerListener соответственно:

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

Укажите название собственного метода

У некоторых атрибутов есть методы установки, которые не совпадают по названию. В таких случаях атрибут можно связать с методом задания значения с помощью аннотации BindingMethods. Аннотация используется с классом и может содержать несколько аннотаций BindingMethod – по одной для каждого переименованного метода. Методы привязки – это аннотации, которые можно добавить в любой класс приложения.

В примере ниже атрибут android:tint связан с методом setImageTintList(ColorStateList), а не с методом 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"),
})

Обычно переименовывать сеттеры в классах фреймворка Android не требуется. Атрибуты уже реализованы с использованием соглашения об именовании для автоматического поиска соответствующих методов.

Предоставьте собственную логику

Для некоторых атрибутов требуется специальная логика связывания. Например, для атрибута android:paddingLeft нет связанного метода setter. Вместо этого используется метод setPadding(left, top, right, bottom). Статический метод адаптера привязки с аннотацией BindingAdapter позволяет настроить вызов метода задания значения для атрибута.

Атрибуты классов фреймворка Android уже имеют аннотации BindingAdapter. Ниже приведен пример адаптера привязки для атрибута 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());
}

Типы параметров имеют значение. Первый параметр определяет тип представления, связанного с атрибутом. Второй параметр определяет тип, принимаемый в выражении привязки для заданного атрибута.

Адаптеры привязки также полезны для других типов настройки. Например, специальный загрузчик можно вызвать из рабочего потока, чтобы загрузить изображение.

Адаптеры также могут получать несколько атрибутов, как показано в примере ниже.

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

Вы можете использовать адаптер в макете, как показано в примере ниже. Обратите внимание, что @drawable/venueError – это ресурс в приложении. Чтобы сделать его допустимым выражением привязки, его нужно заключить в @{}.

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

Адаптер вызывается, если imageUrl и error используются для объекта ImageView, imageUrl – это строка, а error – Drawable. Если вы хотите, чтобы адаптер вызывался при установке любого из атрибутов, установите для необязательного флага requireAll адаптера значение false, как показано в следующем примере:

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

Методы адаптера привязки могут принимать старые значения в своих обработчиках. Метод, принимающий старые и новые значения, должен сначала объявить все старые значения атрибутов, а затем новые, как показано в следующем примере:

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

Обработчики событий можно использовать только с интерфейсами или абстрактными классами с одним абстрактным методом, как показано в следующем примере:

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

Используйте этот обработчик событий в макете следующим образом:

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

Если у прослушивателя несколько методов, его нужно разделить на несколько прослушивателей. Например, у View.OnAttachStateChangeListener есть два метода: onViewAttachedToWindow(View) и onViewDetachedFromWindow(View). Библиотека предоставляет два интерфейса для дифференциации атрибутов и их обработчиков:

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

Поскольку изменение одного прослушивателя может повлиять на другой, вам нужен адаптер, который работает для любого атрибута или для обоих. Вы можете задать для requireAll значение false в аннотации, чтобы указать, что не каждому атрибуту должно быть назначено выражение привязки, как показано в следующем примере:

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

Приведенный выше пример немного сложен, поскольку класс View использует методы addOnAttachStateChangeListener() и removeOnAttachStateChangeListener() вместо метода setter для OnAttachStateChangeListener. Класс android.databinding.adapters.ListenerUtil помогает отслеживать эти прослушиватели, чтобы их можно было удалить в адаптере привязки.

Конверсии объектов

Автоматическое преобразование объектов

Когда выражение привязки возвращает Object, библиотека выбирает метод, используемый для установки значения свойства. Object приводится к типу параметра выбранного метода. Это удобно в приложениях, использующих класс ObservableMap для хранения данных, как показано в следующем примере:

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

Объект userMap в выражении возвращает значение, которое автоматически преобразуется в тип параметра, указанный в методе setText(CharSequence), который используется для установки значения атрибута android:text. Если тип параметра неоднозначен, приведите тип возвращаемого значения в выражении.

Специальные конверсии

В некоторых случаях требуется специальное преобразование между определенными типами. Например, атрибут android:background представления ожидает Drawable, но указанное значение color является целым числом. В примере ниже показан атрибут, который ожидает значение Drawable, но вместо этого указано целое число:

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

Если ожидается значение типа Drawable, а возвращается целое число, преобразуйте int в ColorDrawable. Для преобразования используйте статический метод с аннотацией BindingConversion, как показано ниже.

Kotlin

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

Java

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

Однако типы значений, указанные в выражении привязки, должны быть согласованными. В одном выражении нельзя использовать разные типы, как показано в следующем примере:

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

Дополнительные ресурсы

Дополнительную информацию о привязке данных можно найти в следующих ресурсах.

Образцы

Практические работы

Записи в блоге