是時候擁抱ViewBinding了!!
-
- 一、前言
- 二、初識ViewBinding
- 三、擁抱ViewBinding
-
- 3.1、環境要求
- 3.2、開啟ViewBinding功能
- 3.3、Activity中ViewBinding的使用
-
- 3.3.1、布局中直接的控件
- 3.3.2、布局中使用include
- 3.3.2、布局中使用include和merge
- 3.4、Fragment中使用ViewBinding
- 3.5、自定義Dialog中使用ViewBinding
- 3.6、自定義View中使用ViewBinding
-
- 3.6.1 使用的layout檔案不包含merge
- 3.6.2 使用的layout檔案根标簽為merge
- 3.7、Adapter中使用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的駝峰命名法來擷取的:
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中代碼如下:
是不是很神奇,是不是很簡單。
注意:
當你給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代碼中依舊是可以正常使用以下代碼的:
但是但是!!!運作就會報錯:
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()方法。