Bộ chuyển đổi liên kết

Bộ chuyển đổi liên kết chịu trách nhiệm thực hiện các lệnh gọi khung thích hợp đến đặt giá trị. Một ví dụ là đặt giá trị thuộc tính, chẳng hạn như gọi phương thức setText(). Một ví dụ khác là thiết lập trình nghe sự kiện, chẳng hạn như gọi phương thức setOnClickListener().

Thư viện liên kết dữ liệu cho phép bạn chỉ định phương thức được gọi để đặt giá trị, cung cấp logic liên kết của riêng bạn và chỉ định loại đối tượng được trả về bằng cách sử dụng bộ chuyển đổi.

Đặt giá trị thuộc tính

Bất cứ khi nào giá trị ràng buộc thay đổi, lớp liên kết được tạo phải gọi một phương thức setter trên khung hiển thị bằng biểu thức liên kết. Bạn có thể để Thư viện liên kết dữ liệu tự động xác định phương thức, hoặc bạn có thể khai báo rõ phương thức hoặc cung cấp logic tuỳ chỉnh để chọn một phương thức.

Chọn phương thức tự động

Đối với thuộc tính có tên example, thư viện sẽ tự động tìm phương thức setExample(arg) chấp nhận các loại tương thích làm đối số. Không gian tên của thuộc tính không được xem xét. Chỉ tên và loại thuộc tính được sử dụng khi tìm kiếm một phương thức.

Ví dụ: với biểu thức android:text="@{user.name}", thư viện sẽ tìm một phương thức setText(arg) chấp nhận loại do user.getName() trả về. Nếu loại dữ liệu trả về của user.getName()String, thư viện sẽ tìm phương thức setText() chấp nhận đối số String. Nếu biểu thức trả về một int, thư viện sẽ tìm kiếm phương thức setText() chấp nhận đối số int. Biểu thức phải trả về loại dữ liệu chính xác. Bạn có thể truyền giá trị trả về nếu cần.

Tính năng liên kết dữ liệu hoạt động ngay cả khi không tồn tại thuộc tính nào có tên đã đặt. Bạn có thể tạo thuộc tính cho mọi phương thức setter bằng cách sử dụng tính năng liên kết dữ liệu. Ví dụ: lớp hỗ trợ DrawerLayout không có các thuộc tính nhưng có nhiều phương thức setter. Bố cục sau đây sẽ tự động dùng phương thức setScrimColor(int)addDrawerListener(DrawerListener) làm phương thức setter cho các thuộc tính app:scrimColorapp:drawerListener tương ứng:

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

Chỉ định tên phương thức tuỳ chỉnh

Một số thuộc tính có phương thức setter không khớp theo tên. Trong những trường hợp như vậy, bạn có thể liên kết một thuộc tính với phương thức setter bằng cách sử dụng chú giải BindingMethods. Chú giải này được dùng với một lớp và có thể chứa nhiều chú giải BindingMethod, mỗi chú giải tương ứng với một phương thức đã đổi tên. Phương thức liên kết là các chú giải mà bạn có thể thêm vào bất kỳ lớp nào trong ứng dụng của mình.

Trong ví dụ sau, thuộc tính android:tint được liên kết với phương thức setImageTintList(ColorStateList) chứ không phải với phương thức 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"),
})

Thông thường, bạn không cần đổi tên phương thức setter trong các lớp khung Android. Các thuộc tính đã được triển khai bằng cách sử dụng quy ước tên để tự động tìm các phương thức so khớp.

Cung cấp logic tuỳ chỉnh

Một số thuộc tính cần logic liên kết tuỳ chỉnh. Ví dụ: không có phương thức setter liên kết cho thuộc tính android:paddingLeft. Thay vào đó, phương thức setPadding(left, top, right, bottom) sẽ được cung cấp. Phương thức bộ chuyển đổi liên kết tĩnh có chú giải BindingAdapter cho phép bạn tuỳ chỉnh cách gọi phương thức setter cho một thuộc tính.

Các thuộc tính của các lớp khung Android đã có chú giải BindingAdapter. Ví dụ sau đây cho thấy phương thức điều hợp liên kết (binding adapter) cho thuộc tính 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());
}

Loại thông số rất quan trọng. Tham số đầu tiên xác định loại của thành phần hiển thị liên kết với thuộc tính này. Tham số thứ hai xác định loại được chấp nhận trong biểu thức liên kết cho thuộc tính đã cho.

Bộ chuyển đổi liên kết cũng hữu ích cho các loại tuỳ chỉnh khác. Ví dụ: bạn có thể gọi trình tải tuỳ chỉnh từ một luồng worker để tải hình ảnh.

Bạn cũng có thể có các bộ chuyển đổi nhận nhiều thuộc tính, như trong ví dụ sau:

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

Bạn có thể sử dụng trình chuyển đổi trong bố cục, như trong ví dụ sau. Hãy lưu ý rằng @drawable/venueError tham chiếu đến một tài nguyên trong ứng dụng của bạn. Việc xung quanh tài nguyên bằng @{} sẽ khiến tài nguyên này trở thành một biểu thức liên kết hợp lệ.

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

Bộ chuyển đổi được gọi nếu imageUrlerror được dùng cho một đối tượng ImageView, imageUrl là một chuỗi và error là một Drawable. Nếu bạn muốn bộ chuyển đổi được gọi khi đặt bất kỳ thuộc tính nào, hãy đặt cờ requireAll (không bắt buộc) của bộ chuyển đổi thành false, như minh hoạ trong ví dụ sau:

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

Phương thức của bộ chuyển đổi liên kết có thể lấy các giá trị cũ trong trình xử lý. Phương thức nhận các giá trị cũ và mới phải khai báo tất cả các giá trị cũ cho các thuộc tính trước, sau đó là các giá trị mới, như trong ví dụ sau:

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

Bạn chỉ có thể dùng trình xử lý sự kiện với giao diện hoặc lớp trừu tượng bằng một phương thức trừu tượng, như trong ví dụ sau:

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

Sử dụng trình xử lý sự kiện này trong bố cục của bạn như sau:

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

Khi một trình nghe có nhiều phương thức, bạn phải chia thành nhiều trình nghe. Ví dụ: View.OnAttachStateChangeListener có hai phương thức: onViewAttachedToWindow(View)onViewDetachedFromWindow(View). Thư viện cung cấp 2 giao diện để phân biệt các thuộc tính và trình xử lý:

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

Vì việc thay đổi một trình nghe có thể ảnh hưởng đến trình nghe còn lại, nên bạn cần có một bộ chuyển đổi hoạt động cho một trong hai thuộc tính hoặc cả hai. Bạn có thể đặt requireAll thành false trong chú giải để chỉ định rằng không phải thuộc tính nào cũng phải được gán một biểu thức liên kết, như minh hoạ trong ví dụ sau:

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

Ví dụ trên hơi phức tạp vì lớp View sử dụng phương thức addOnAttachStateChangeListener()removeOnAttachStateChangeListener() thay vì phương thức setter cho OnAttachStateChangeListener. Lớp android.databinding.adapters.ListenerUtil giúp theo dõi các trình nghe này để có thể xoá trong bộ chuyển đổi liên kết.

Chuyển đổi đối tượng

Tự động chuyển đổi đối tượng

Khi một Object được trả về từ một biểu thức liên kết, thư viện sẽ chọn phương thức dùng để đặt giá trị của thuộc tính. Object được truyền thành loại tham số của phương thức đã chọn. Hành vi này thuận tiện trong các ứng dụng dùng lớp ObservableMap để lưu trữ dữ liệu, như trong ví dụ sau:

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

Đối tượng userMap trong biểu thức sẽ trả về một giá trị. Giá trị này được tự động truyền đến loại tham số tìm thấy trong phương thức setText(CharSequence) dùng để đặt giá trị của thuộc tính android:text. Nếu loại tham số không rõ ràng, hãy truyền loại dữ liệu trả về trong biểu thức.

Lượt chuyển đổi tuỳ chỉnh

Trong một số trường hợp, bạn phải có lượt chuyển đổi tuỳ chỉnh giữa các loại cụ thể. Ví dụ: thuộc tính android:background của khung hiển thị yêu cầu Drawable, nhưng giá trị color được chỉ định lại là một số nguyên. Ví dụ sau đây cho thấy một thuộc tính yêu cầu Drawable, nhưng lại cung cấp một số nguyên:

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

Bất cứ khi nào Drawable dự kiến và một số nguyên được trả về, hãy chuyển đổi int thành ColorDrawable. Để thực hiện việc chuyển đổi, hãy sử dụng phương thức tĩnh có chú giải BindingConversion như sau:

Kotlin

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

Java

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

Tuy nhiên, các loại giá trị được cung cấp trong biểu thức liên kết phải nhất quán. Bạn không thể sử dụng nhiều loại trong cùng một biểu thức, như trong ví dụ sau:

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

Tài nguyên khác

Để tìm hiểu thêm về liên kết dữ liệu, hãy xem các tài nguyên sau.

Mẫu

Lớp học lập trình

Bài đăng trên blog