Kotlin Android Extensions: Sử dụng View Binding đúng cách

Làm thế nào để dụng View Binding trong các classes khác nhau như Activities, Fragments và view.

Nếu bạn sử dụng Kotlin Android Extensions, có lẽ bạn đã được nghe về các tính năng của View Binding. “Say goodbye to findViewById” (nói tạm biệt với findviewById) bởi Antonio Leiva thì rất phổ biến trên các bài viết. Hiển nhiên của việc sử dụng View Binding là tạo ra các dòng code ngắn gọn xúc tích hơn. Bạn không phải sử dụng findViewById hay chú thích @BindView của thư viện ButterKnife. Ít hơn một dòng code cho mỗi view được sử dụng. Đó là tín hiệu tuyệt vời! Nhưng đó chỉ là Hummock!

Tôi sẽ giới thiệu cho bạn về Bummock: _$_findCachedViewById

Khi bạn sử dụng KTX và import các View đã được khai báo trong xml vào các classes, trình biên dịch sẽ tạo ra các phương thức đặc biệt cho bạn, đó là $findCachedViewById. Để thấy điều đó, bạn cần mở Kotlin bytecode và dịch ngược nó đến java như trong mô tả ở đây.

Tôi muốn đào sâu hơn để chỉ ra điểm chung mà không được rõ ràng.

Trong tài liệu chính thức được nói là:

“Android Extensions plugin supports different kinds of containers. The most basic ones are Activity, Fragment and View, but you can turn (virtually) any class to an Android Extensions container by implementing the LayoutContainer interface…”

ta có thể hiểu đoạn này là: Trong Android Extensions plugin thì hỗ trợ nhiều loại vùng chứa khác nhau. Cơ bản nhất là, Activity, Fragment và View, nhưng bạn cũng có thể biến class bất kỳ nào thành một Android Extensions container và thực thi giao diện LayoutContainer...

Khi bạn sử dụng View Binding extensions trong một classes khác “Activity, Fragment hay View” bạn phải thực thi LayoutContainer interface. Điều đó không đúng, nếu bạn đưa 1 view instance đến classes của bạn, bạn có thể gọi KTX view của bạn một cách đơn giản, dựa trên instance đã được đưa vào. Nếu bạn ở trong một class ViewHolder, bạn có thể trực tiếp sử dụng itemView:

import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.synthetic.main.recycler_item.view.*

class IcebergViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    fun bind(item: GlacierItem){
        itemView.textViewHummock.text = item.universalName ?: "I am not cached"
    }
    
}

ĐỪNG BAO GIỜ LÀM NÓ!

Điều này ngăn cản trình biên dịch lưu vào bộ nhớ đệm của bạn. Đây là bằng chứng:

public final class IcebergViewHolder extends ViewHolder {
   
   public final void bind(@NotNull GlacierItem item) {
      Intrinsics.checkParameterIsNotNull(item, "item");
      View var10000 = this.itemView;
      Intrinsics.checkExpressionValueIsNotNull(this.itemView, "itemView");
      
      AppCompatTextView var2 = (AppCompatTextView)var10000.findViewById(id.textViewHummock);
      
      Intrinsics.checkExpressionValueIsNotNull(var2, "itemView.textViewHummock");
      String var10001 = item.getUniversalName();
      var2.setText(var10001 != null ? (CharSequence)var10001 : (CharSequence)"I am not cached");
   }
   
}

Dịch ngược Kotlin bytecode được tạo ra đến java, bạn có thể thấy findViewById được gọi. Vì thế bạn cần cố gắng tìm kiếm giải pháp và xem cái bạn cần thực thi LayoutContainer interface. Bạn cũng cần chú ý cái bẫy ở đó. Bây giờ thì ViewHolder của bạn trông như thế này:

import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.synthetic.main.recycler_item.view.*

class IcebergViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), LayoutContainer {

    override val containerView: View?
        get() = itemView

    fun bind(item: GlacierItem){
        itemView.textViewHummock.text = item.universalName ?: "I look like I am cached but I'm not"
    }
    
}

Ngay khi bạn thực hiện LayoutContainer interface, biến findViewCache và các phương thức findCachedViewById & clearFindViewByIdCache tương ứng được tạo ra. Nhưng nếu bạn có một cái nhìn gần hơn, không có cái nào được sử dụng trong phương thức bind hay ngược lại bất kỳ chỗ nào. Vì thế cơ chế cache vẫn không làm việc.

public final class IcebergViewHolder extends ViewHolder implements LayoutContainer {
   private HashMap _$_findViewCache;

   @Nullable
   public View getContainerView() {
      return this.itemView;
   }

   public final void bind(@NotNull GlacierItem item) {
      Intrinsics.checkParameterIsNotNull(item, "item");
      View var10000 = this.itemView;
      Intrinsics.checkExpressionValueIsNotNull(this.itemView, "itemView");
      
      AppCompatTextView var2 = (AppCompatTextView)var10000.findViewById(id.textViewHummock);
      
      Intrinsics.checkExpressionValueIsNotNull(var2, "itemView.textViewHummock");
      String var10001 = item.getUniversalName();
      var2.setText(var10001 != null ? (CharSequence)var10001 : (CharSequence)"I look like I am cached but I'm not");
   }
   
   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
         View var10000 = this.getContainerView();
         if (var10000 == null) {
            return null;
         }

         var2 = var10000.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if (this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }
}

Cách đúng như sau:

import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.recycler_item.*

class IcebergViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), LayoutContainer {

    override val containerView: View?
        get() = itemView

    fun bind(item: GlacierItem){
        textViewHummock.text = item.universalName ?: "You did it!"
    }

}

Bạn cần gỡ bỏ itemView trước textViewHummock.

Từ:

import kotlinx.android.synthetic.main.recycler_item.view.*

đến:

import kotlinx.android.synthetic.main.recycler_item.*

Còn đây là phần giải mã:

public final class IcebergViewHolder extends ViewHolder implements LayoutContainer {
   private HashMap _$_findViewCache;

   @Nullable
   public View getContainerView() {
      return this.itemView;
   }

   public final void bind(@NotNull GlacierItem item) {
      Intrinsics.checkParameterIsNotNull(item, "item");
     
      AppCompatTextView var10000 = (AppCompatTextView)this._$_findCachedViewById(id.textViewMatch);
     
      Intrinsics.checkExpressionValueIsNotNull(var10000, "textViewMatch");
      String var10001 = item.getUniversalName();
      var10000.setText(var10001 != null ? (CharSequence)var10001 : (CharSequence)"You did it!");
   }

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
         View var10000 = this.getContainerView();
         if (var10000 == null) {
            return null;
         }

         var2 = var10000.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if (this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }
}

Bây giờ nó đã sử dụng _$findCachedViewById trong phương thức bind.

trong ViewHolder của bạn, đừng bao giờ viết dòng sau:

itemView.textViewBlaBla.text = myItem.blaBla

Nó sẽ giảm performance view holder của bạn. Bạn hãy sử dụng LayoutContainer interface!

Cảm ơn các bạn đã theo dõi. Bài viết được dịch từ kotlin-android-extensions-using-view-binding-the-right-way.