是時候擁抱ViewBinding了!!

沉舟側畔千帆過,
病樹前頭萬木春。
– 唐·劉禹錫

一、前言

隨着Android Studio 3.6的正式發佈,我義無反顧的走在了更新嚐鮮的前列。AS的升級一如往常的順利,重啓後就進入了令人血脈噴張的 Gradle 升級的環節,需要從3.5.1升級到3.6.0。果不其然,出問題了!!

ButterKnife居然報錯,日誌如下:

D:\xxx\libbase\component\dialog\BottomDialog.java:33:     : Attempt to use @BindView for an already bound ID 0 on 'mTvNegative'. (com.xxx.libbase.component.dialog.BottomDialog.mLayoutContent)
    ViewGroup mLayoutContent;

我真是摸不着頭腦啊。解決吧,升級ButterKnife、翻資料、找issue、看源碼等等等等。最終老天不負有心人,我將Gradle版本回退了,一切都回歸平靜。【如果有解決辦法的請告知我,感激不盡】

二、初識ViewBinding

它和ButterKnife一樣都是爲了省去findViewById()這樣的重複代碼。其實在2019谷歌開發者峯會上對ViewBinding就已經有所耳聞了,layout中更新控件ID後立刻可以在Activity中引用到,這絕對比ButterKnife需要編譯、需要區分R和R2要舒服的多。
上面升級到3.6.0就是爲了使用它,然而現實永遠這麼的殘酷,十之八九不盡人意,ViewBinding和ButterKnife看來只能二選一了。

三、擁抱ViewBinding

關於ViewBinding的文檔,官方寫的很詳細,請看 視圖綁定 。本文一切從簡,主要說下Google官方沒有提到的一些問題。

3.1、環境要求

  • Android Studio版本3.6及以上
  • Gradle 插件版本3.6.0及以上

3.2、開啓ViewBinding功能

ViewBinding支持按模塊啓用,在模塊的build.gradle文件中添加如下代碼:

android {
        ...
        viewBinding {
            enabled = true
        }
}    

3.3、Activity中ViewBinding的使用

//之前設置視圖的方法
setContentView(R.layout.activity_main);

//使用ViewBinding後的方法
mBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());

可以看到,當你使用了ViewBinding後,針對你的activity_main.xml文件,會自動幫你生成一個ActivityMainBinding.java文件(該文件在build/generated/data_binding_base_class_source_out/xxx…目錄下),也就是佈局文件的駝峯命名法加上一個Binding後綴,然後在Activity中直接使用就可以。

3.3.1、佈局中直接的控件

當我們在佈局中添加一個id爲 tv_text 的TextView後,直接在Activity中使用mBinding.tvText即可拿到該控件。如下所示,可以看到也是以控件ID的駝峯命名法來獲取的:

mBinding.tvText.setText("是你得不到的ViewBinding");

3.3.2、佈局中使用include

例如我們有個layout_comment.xml的佈局,佈局中有id爲tv_include的TextView,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_include"
        android:text="這就是測試啊"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

然後在activity_main.xml文件中include該佈局:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <include
        android:id="@+id/layout_include"
        layout="@layout/layout_comment" />

</androidx.constraintlayout.widget.ConstraintLayout>

那麼此時我們如何使用到layout_comment.xml佈局中的TextView控件呢,首先include標籤需要聲明id,例如layout_include,然後Activity中代碼如下:

mBinding.layoutInclude.tvInclude.setText("這就是你的不對了");

是不是很神奇,是不是很簡單。

注意:
當你給layout_comment.xml的根佈局再添加id(比如添加了layout_xxx的ID)的時候,此時會報錯:

java.lang.NullPointerException: Missing required view with ID: layout_xxx

3.3.2、佈局中使用include和merge

我們將上文的layout_comment.xml稍作修改,根佈局使用merge標籤,其他不做修改:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_include"
        android:text="這就是測試啊"
        android:gravity="end"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</merge>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

        <include
            android:id="@+id/layout_include"
            layout="@layout/layout_comment" />

</androidx.constraintlayout.widget.ConstraintLayout>

activity_main.xml文件中使用include添加該佈局後,在java代碼中依舊是可以正常使用以下代碼的:

mBinding.layoutInclude.tvInclude.setText("會不會出現問題呀");

但是但是!!!運行就會報錯:

java.lang.NullPointerException: Missing required view with ID: layoutInclude

要是把include標籤的id去掉的話,這時mBinding中也是找不到tvInclude這個控件呀,怎麼辦??
之前是不是說過,每個layout文件都會對應一個Binding文件,那麼layout_comment.xml,肯定也有一個LayoutCommentBinding.java文件,我們去看下這個文件的源代碼,裏面有個可疑的方法,bind()方法:

 @NonNull
  public static LayoutCommentBinding bind(@NonNull View rootView) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    String missingId;
    missingId: {
      TextView tvInclude = rootView.findViewById(R.id.tv_include);
      if (tvInclude == null) {
        missingId = "tvInclude";
        break missingId;
      }
      return new LayoutCommentBinding(rootView, tvInclude);
    }
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }

所以對於含有merge標籤的佈局我們可以使用bind()方法來綁定到根佈局上,在這裏,根佈局就是mBinding.getRoot()了。所以代碼如下:

        //這麼寫不可以
        //mBinding.layoutInclude.tvInclude.setText("會不會出現問題呀");

        LayoutCommentBinding commentBinding = LayoutCommentBinding.bind(mBinding.getRoot());
        commentBinding.tvInclude.setText("這就不會出現問題了吧");

同時需要注意: include標籤不可以有id

3.4、Fragment中使用ViewBinding

在Fragment的**onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)**方法中:

//原來的寫法
return inflater.inflate(R.layout.fragment_blank, container, false);

//使用ViewBinding的寫法
mBinding = FragmentBlankBinding.inflate(inflater);
return mBinding.getRoot();

拿到FragmentBlankBinding的對象後,更新數據的都和之前一樣了。

3.5、自定義Dialog中使用ViewBinding

dialog中使用和Activity以及Fragment一樣,直接使用單參數的inflate()方法即可,僞代碼如下:

public class MyDialog extends Dialog {

    protected View mView;
    protected DialogBottomBinding mBinding;
    
    public MyDialog(@NonNull Context context, @StyleRes int themeResId) {
        super(context, themeResId);

        //原來的寫法
		mView = View.inflate(getContext(), getLayoutId(), null);

		//使用ViewBinding的寫法
        mBinding = DialogBottomBinding.inflate(getLayoutInflater());
        mView = mBinding.getRoot();
        
        setContentView(mView);
    }
}

3.6、自定義View中使用ViewBinding

我在重構工程的時候發現了自定義視圖中其實有很多問題,這裏把這兩種常見的方法總結下:

3.6.1 使用的layout文件不包含merge

這裏直接貼出來代碼吧,就是自定義了一個LinearLayout然後往其中添加了一個佈局,該佈局是view_my_layout.xml文件,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="這是自定義佈局"
        android:textSize="50sp" />

</androidx.constraintlayout.widget.ConstraintLayout>

會生成一個對應的ViewMyLayoutBinding.java文件,看下文MyLinearLayout 代碼:
init1、2、3、4是使用inflate來導入layout佈局的寫法,全部可以正常顯示自定義的佈局。
init10、11、12是使用ViewBinding的寫法,10無法正常顯示視圖,11和12是兩種不同的寫法,道理一樣。


public class MyLinearLayout extends LinearLayout {
    public MyLinearLayout(Context context) {
        this(context, null);
    }

    public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

//        init1();
//        init2();
//        init3();
        init4();
    }

    private void init1() {
        inflate(getContext(), R.layout.view_my_layout, this);
    }

    private void init2() {
        View view = LayoutInflater.from(getContext()).inflate(R.layout.view_my_layout, this);
    }

    //和init2()方法相等
    private void init3() {
        View view = LayoutInflater.from(getContext()).inflate(R.layout.view_my_layout, this, true);
    }

    private void init4() {
        View view = LayoutInflater.from(getContext()).inflate(R.layout.view_my_layout, this, false);
        addView(view);
    }

    //視圖異常,佈局無法填充滿
    private void init10() {
        ViewMyLayoutBinding binding = ViewMyLayoutBinding.inflate(LayoutInflater.from(getContext()));
        addView(binding.getRoot());
    }

    private void init11() {
        ViewMyLayoutBinding binding = ViewMyLayoutBinding.inflate(LayoutInflater.from(getContext()), this, true);
    }

    private void init12() {
        ViewMyLayoutBinding binding = ViewMyLayoutBinding.inflate(LayoutInflater.from(getContext()), this, false);
        addView(binding.getRoot());
    }

}

3.6.2 使用的layout文件根標籤爲merge

我們添加一個view_my_layout_merge.xml文件,根標籤爲merge:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="這是自定義merge"
        android:textSize="50sp" />

</merge>

此時在MyLinearLayout.java中使用的話,正確寫法是init20()方法:


    private void init20() {
        ViewMyLayoutMergeBinding binding = ViewMyLayoutMergeBinding.inflate(LayoutInflater.from(getContext()), this);
    }

    //沒有效果,可以理解爲還沒有rootView
    private void init21() {
        ViewMyLayoutMergeBinding binding = ViewMyLayoutMergeBinding.bind(this);
    }

我們對比下使用merge標籤和不使用merge標籤所對應的Binding文件:
使用merge標籤生成的代碼大致如下,inflate()方法最終調用了bind()方法:

  @NonNull
  public static ViewMyLayoutMergeBinding inflate(@NonNull LayoutInflater inflater,
      @NonNull ViewGroup parent) {
    if (parent == null) {
      throw new NullPointerException("parent");
    }
    inflater.inflate(R.layout.view_my_layout_merge, parent);
    return bind(parent);
  }

  @NonNull
  public static ViewMyLayoutMergeBinding bind(@NonNull View rootView) {
    if (rootView == null) {
      throw new NullPointerException("rootView");
    }
    return new ViewMyLayoutMergeBinding(rootView);
  }

不使用merge標籤的Binding代碼如下,inflate(@NonNull LayoutInflater inflater) 調用了 inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, boolean attachToParent) 方法,最終調用了**bind(@NonNull View rootView)**方法:

  @NonNull
  public static ViewMyLayoutBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static ViewMyLayoutBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.view_my_layout, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static ViewMyLayoutBinding bind(@NonNull View rootView) {
    if (rootView == null) {
      throw new NullPointerException("rootView");
    }
    return new ViewMyLayoutBinding((ConstraintLayout) rootView);
  }

這裏基本就把所有的自定義視圖中使用ViewBinding的方法總結了一下,主要是inflate方法的使用,其實就是幫我們封裝了下inflate方法,如果不知道使用哪個方法的話可以查看生成的ViewBinding源代碼,一眼就能明瞭我們之前的寫法對應的是現在的哪個方法了。
如果還不熟悉的話,請翻閱其他inflate的相關資料,相信你會有很大收貨。當然了當你熟悉inflate方法之後,下面的文章其實可以沒必要看了。

3.7、Adapter中使用ViewBinding

在RecyclerView結合Adapter的例子中我們再使用ViewBinding來嘗試下,直接貼Adapter的代碼:

public class MainAdapter extends RecyclerView.Adapter<MainAdapter.ViewHolder> {

    private List<String> mList;

    public MainAdapter(List<String> list) {
        mList = list;
    }

    @NonNull
    @Override
    public MainAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        //之前的寫法
        //View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_comment, parent, false);
        //ViewHolder holder = new ViewHolder(view);

        //使用ViewBinding的寫法
        LayoutCommentBinding commentBinding = LayoutCommentBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
        ViewHolder holder = new ViewHolder(commentBinding);
        return holder;
    }

    @Override
    public void onBindViewHolder(@NonNull MainAdapter.ViewHolder holder, int position) {
        holder.mTextView.setText(mList.get(position));
    }

    @Override
    public int getItemCount() {
        return mList.size();
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        TextView mTextView;

        //之前的寫法
        //public ViewHolder(@NonNull View itemView) {
        //    super(itemView);
        //    mTextView = itemView.findViewById(R.id.tv_include);
        //}

        //使用ViewBinding的寫法
        ViewHolder(@NonNull LayoutCommentBinding commentBinding) {
            super(commentBinding.getRoot());
            mTextView = commentBinding.tvInclude;
        }

    }
}

只需要注意兩方面:

  • ViewHolder的構造器參數改爲使用的Binding對象
  • 實例化ViewHolder的時候傳入相應的Binding對象

四、關於封裝

大概瞭解了ViewBinding後,我們可以考慮將其完全封裝在BaseActivity(BaseFragment、BaseDialog、BaseView等)等底層的公共類中,省去手動實例化相應ViewBinding類的這一過程。
首先可以使用泛型類,每個具體的Activity繼承BaseActivity,並傳遞進去對應的ViewBinding,然後反射對應的inflate()方法獲取到ViewBinding實例。拿到實例後就可以對控件爲所欲爲了不是!!

五、總結

使用ViewBinding的話,其實很簡單,新建xxx.xml佈局後就會產生一個對應的 xxxBinding.java的文件,實例化xxxBinding只需要調用它自身的inflate()方法即可。
注意不同情況下使用不同的inflate()方法,以及使用了merge標籤情況下的bind()方法,以及使用merge標籤佈局和其他正常xxxLayout佈局所產生的不同的inflate()方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章