Adaptor binding

Adaptor binding bertanggung jawab membuat panggilan framework yang sesuai untuk menetapkan nilai. Salah satu contohnya adalah menetapkan nilai properti, seperti memanggil metode setText(). Contoh lainnya adalah menetapkan pemroses peristiwa, seperti memanggil metode setOnClickListener().

Dengan Library Data Binding, Anda dapat menentukan metode yang dipanggil untuk menetapkan nilai, menyediakan logika binding Anda sendiri, dan menentukan jenis objek yang ditampilkan menggunakan adaptor.

Menetapkan nilai atribut

Setiap kali nilai terikat berubah, class binding yang dihasilkan harus memanggil metode penyetel pada tampilan yang memiliki ekspresi binding. Anda dapat mengizinkan Library Data Binding secara otomatis menentukan metode, atau Anda dapat mendeklarasikan metode secara eksplisit atau memberikan logika kustom untuk memilih metode.

Pemilihan metode otomatis

Untuk atribut bernama example, library akan otomatis menemukan metode setExample(arg) yang menerima jenis yang kompatibel sebagai argumen. Namespace atribut tidak dipertimbangkan. Hanya nama dan jenis atribut yang digunakan saat menelusuri metode.

Misalnya, dengan ekspresi android:text="@{user.name}", library akan mencari metode setText(arg) yang menerima jenis yang ditampilkan oleh user.getName(). Jika jenis nilai yang ditampilkan user.getName() adalah String, library akan mencari metode setText() yang menerima argumen String. Jika ekspresi menampilkan int, library akan menelusuri metode setText() yang menerima argumen int. Ekspresi harus menampilkan jenis yang benar. Anda dapat mengirimkan nilai hasil jika perlu.

Data binding berfungsi meskipun tidak ada atribut dengan nama yang ditentukan. Anda dapat membuat atribut untuk penyetel apa pun dengan menggunakan data binding. Misalnya, class dukungan DrawerLayout tidak memiliki atribut, tetapi memiliki banyak penyetel. Tata letak berikut secara otomatis menggunakan metode setScrimColor(int) dan addDrawerListener(DrawerListener) sebagai penyetel untuk masing-masing atribut app:scrimColor dan app:drawerListener:

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

Menentukan nama metode kustom

Beberapa atribut memiliki penyetel yang tidak melakukan pencocokan berdasarkan nama. Dalam situasi ini, atribut dapat dikaitkan dengan penyetel menggunakan anotasi BindingMethods. Anotasi ini digunakan dengan class dan dapat berisi beberapa anotasi BindingMethod, satu untuk setiap metode yang diganti namanya. Metode binding adalah anotasi yang dapat Anda tambahkan ke class mana pun di aplikasi.

Pada contoh berikut, atribut android:tint dikaitkan dengan metode setImageTintList(ColorStateList) —bukan dengan metode 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"),
})

Biasanya, Anda tidak perlu mengganti nama penyetel di class framework Android. Atribut tersebut sudah diimplementasikan menggunakan konvensi nama untuk menemukan metode yang cocok secara otomatis.

Menyediakan logika kustom

Beberapa atribut memerlukan logika binding kustom. Misalnya, tidak ada penyetel terkait untuk atribut android:paddingLeft. Sebagai gantinya, metode setPadding(left, top, right, bottom) disediakan. Metode adaptor binding statis dengan anotasi BindingAdapter memungkinkan Anda menyesuaikan cara pemanggilan penyetel untuk atribut.

Atribut class framework Android sudah memiliki anotasi BindingAdapter. Contoh berikut menunjukkan adaptor binding untuk atribut 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());
}

Jenis parameter itu penting. Parameter pertama menentukan jenis tampilan yang dikaitkan dengan atribut. Parameter kedua menentukan jenis yang diterima dalam ekspresi binding untuk atribut yang diberikan.

Adaptor binding juga berguna untuk jenis penyesuaian lainnya. Misalnya, loader kustom dapat dipanggil dari thread pekerja untuk memuat gambar.

Anda juga dapat memiliki adaptor yang menerima beberapa atribut, seperti yang ditunjukkan dalam contoh berikut:

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

Anda dapat menggunakan adaptor di tata letak, seperti yang ditunjukkan pada contoh berikut. Perlu diperhatikan bahwa @drawable/venueError merujuk pada resource dalam aplikasi Anda. Mengapit resource dengan @{} membuatnya menjadi ekspresi binding yang valid.

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

Adaptor dipanggil jika imageUrl dan error digunakan untuk objek ImageView, imageUrl adalah string, dan error adalah Drawable. Jika Anda ingin adaptor dipanggil saat salah satu atribut ditetapkan, setel flag requireAll opsional pada adaptor ke false, seperti ditunjukkan dalam contoh berikut:

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

Metode adaptor binding bisa menggunakan nilai lama dalam pengendalinya. Metode yang mengambil nilai lama dan baru harus mendeklarasikan semua nilai lama untuk atribut terlebih dahulu, kemudian diikuti dengan nilai baru, seperti yang ditunjukkan dalam contoh berikut:

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

Pengendali peristiwa hanya dapat digunakan dengan antarmuka atau class abstrak yang memiliki satu metode abstrak, seperti ditunjukkan dalam contoh berikut:

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

Gunakan pengendali peristiwa ini dalam tata letak Anda sebagai berikut:

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

Jika memiliki beberapa metode, suatu pemroses harus dipecah menjadi beberapa pemroses. Misalnya, View.OnAttachStateChangeListener memiliki dua metode: onViewAttachedToWindow(View) dan onViewDetachedFromWindow(View). Library ini menyediakan dua antarmuka untuk membedakan atribut dan pengendali untuk keduanya:

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

Karena mengubah satu pemroses dapat memengaruhi pemroses yang lain, Anda memerlukan adaptor yang berfungsi untuk salah satu atribut atau keduanya. Anda dapat menetapkan requireAll ke false dalam anotasi untuk menentukan bahwa tidak setiap atribut harus diberi ekspresi binding, seperti yang ditunjukkan dalam contoh berikut:

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

Contoh di atas sedikit rumit karena class View menggunakan metode addOnAttachStateChangeListener() dan removeOnAttachStateChangeListener(), bukan metode penyetel untuk OnAttachStateChangeListener. Class android.databinding.adapters.ListenerUtil membantu melacak pemroses ini sehingga dapat dihapus di adaptor binding.

Konversi objek

Konversi objek otomatis

Saat Object ditampilkan dari ekspresi binding, library akan memilih metode yang digunakan untuk menetapkan nilai properti. Object ditransmisikan ke jenis parameter metode yang dipilih. Perilaku ini praktis pada aplikasi yang menggunakan class ObservableMap untuk menyimpan data, seperti yang ditunjukkan dalam contoh berikut:

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

Objek userMap dalam ekspresi ini menampilkan nilai, yang otomatis ditransmisikan ke jenis parameter yang ditemukan dalam metode setText(CharSequence) yang digunakan untuk menetapkan nilai atribut android:text. Jika jenis parameter ambigu, transmisikan jenis nilai yang ditampilkan dalam ekspresi.

Konversi kustom

Dalam beberapa situasi, konversi kustom diperlukan di antara jenis-jenis tertentu. Misalnya, atribut android:background dari sebuah tampilan mengharapkan Drawable, tetapi nilai color yang ditentukan adalah bilangan bulat. Contoh berikut menunjukkan atribut yang mengharapkan Drawable, tetapi yang diberikan adalah bilangan bulat:

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

Setiap kali Drawable diharapkan dan bilangan bulat ditampilkan, konversikan int menjadi ColorDrawable. Untuk melakukan konversi, gunakan metode statis dengan anotasi BindingConversion, sebagai berikut:

Kotlin

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

Java

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

Namun, jenis nilai yang diberikan dalam ekspresi binding harus konsisten. Anda tidak dapat menggunakan jenis yang berbeda dalam ekspresi yang sama, seperti yang ditunjukkan pada contoh berikut:

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

Referensi lainnya

Untuk mempelajari data binding lebih lanjut, lihat referensi berikut.

Contoh

Codelab

Postingan blog