1.背景
在日常app界面開發(fā)過程中,經(jīng)常會遇到如下場景:
-
場景1:升級到較新版本Android Studio后,在使用R.id.xxxx時會出現(xiàn)警告提示:
Resource IDs will be non-final by default in Android Gradle Plugin version 8.0, avoid using them as annotation attributes
-
場景2:在日常UI界面開發(fā)過程中路幸,每個界面的View的id命名是一個相對頭疼的問題铡买。為了較好的區(qū)分每個id镣隶,通常id的命名會很長魂那,導(dǎo)致id寫起來啰嗦隙笆,也缺乏美觀锌蓄。不同的開發(fā)人員有各種喜好的命名風(fēng)格,導(dǎo)致命名上相對混亂撑柔。例如類似這種:
R.id.app_item_game_comment_detail_bottom_user_name
場景3:視圖變量聲明后瘸爽,使用過程中經(jīng)常出現(xiàn)空指針異常。當(dāng)前界面布局文件中沒有該id铅忿,View聲明中確使用了這個id剪决,導(dǎo)致空指針,程序崩潰退出檀训。
場景4:butterknife 已經(jīng)停止維護(hù)了柑潦,不再升級更新。不想繼續(xù)使用 butterknife峻凫,但需要其它方法代替
有什么方法可以預(yù)防或者減少類似的問題發(fā)生呢渗鬼?答案是肯定的:ViewBinding
2.什么是ViewBinding
View Binding是Android Studio 3.6 推出的新特性,旨在替代findViewById(內(nèi)部實(shí)現(xiàn)還是使用findViewById)荧琼。算是一個語法糖譬胎。
通過ViewBinding差牛,可以更輕松地編寫可與視圖交互的代碼。在模塊中啟用ViewBinding之后银择,系統(tǒng)會為該模塊中的每個 XML 布局文件生成一個綁定類多糠。綁定類的實(shí)例包含對在相應(yīng)布局中具有 ID 的所有視圖的直接引用。
在大多數(shù)情況下浩考,ViewBinding可以做到替代 findViewById夹孔。
3.ViewBinding怎么使用
啟用Viewbinding功能:在模塊build.gradle文件android節(jié)點(diǎn)下添加如下代碼
android {
......
buildFeatures {
viewBinding true
}
}
重新編譯后系統(tǒng)會為每個布局文件生成對應(yīng)的Binding類。該類中包含對應(yīng)布局中具有 ID 的所有視圖的直接引用析孽。
文件生成規(guī)則:將布局文件名轉(zhuǎn)成駝峰命名并添加以Binding結(jié)尾的類搭伤。例如activity_main.xml生成的類是ActivityMainBinding.java。類的字段是對應(yīng)布局文件中的id袜瞬。字段名生成規(guī)則和類名類似怜俐。
生成類的目錄在模塊根目錄/build/generated/data_binding_base_class_source_out下。如下圖:
3.1 在Activity中使用
在MainActivity布局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity">
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="come on" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
MainActivity中使用:
ActivityMainBinding mViewBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mViewBinding.getRoot());
...
}
使用的時候在Activity的onCreate方法里調(diào)用其靜態(tài)inflate方法邓尤,返回ViewBinding實(shí)例拍鲤,通過ViewBinding實(shí)例可以直接訪問布局文件中帶id的控件,比如上面的TextView汞扎,mViewBinding.tvTextView
如果想在生成綁定類時忽略某個布局文件季稳,將tools:viewBindingIgnore="true"屬性添加到相應(yīng)布局文件的根視圖中。
3.2 在Fragment中使用
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
FragmentViewBindBinding binding = FragmentViewBindBinding.inflate(inflater, container, false);
return binding.getRoot();
}
3.3 在Dialog中的使用
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.4 在 Adapter 中的使用
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
ItemViewBinding viewBinding = ItemViewBinding.inflate(LayoutInflater.from(mContext));
return new MyViewHolder(viewBinding);
}
3.5 自定義View中使用
如果我們的自定義View中使用了layout布局澈魄,比如layout_my_view.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>
會生成一個名為LayoutMyViewViewBinding.java文件,在自定義View通過如下方式綁定痹扇,
public class MyView extends View {
public MyView (Context context) {
this(context, null);
}
public MyView (Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView (Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutMyViewViewBinding viewBinding = LayoutMyViewViewBinding.inflate(LayoutInflater.from(getContext()), this, true);
}
}
如果自定義View布局文件中使用merge標(biāo)簽铛漓,
<?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>
此時要寫成下面這個樣子,
LayoutMyViewViewBindingviewBinding=LayoutMyViewViewBinding.inflate(LayoutInflater.from(context),this);
include 標(biāo)簽的使用
include 標(biāo)簽不帶 merge 標(biāo)簽鲫构,需要給 include 標(biāo)簽添加 id, 直接使用 id 即可浓恶,用法如下所示。
<include
android:id="@+id/include"
layout="@layout/layout_include_item" />
ActivityMainBinding binding = ActivityMainBinding.inflate(layoutInflater);
binding.include.includeTvTitle.setText("使用 include 布局中的控件, 不包含 merge");
include 標(biāo)簽帶 merge 標(biāo)簽芬迄,需要通過bind()將merge布局綁定到主布局上问顷,用法如下所示。
<include
layout="@layout/layout_merge_item" />
ActivityMainBinding binding = ActivityMainBinding.inflate(layoutInflater);
LayoutMergeItemBinding mergeItemBinding = LayoutMergeItemBinding.bind(binding.getRoot())禀梳;
mergeItemBinding.mergeTvTitle.setText("使用 include 布局中的控件, 包含 merge");
4.原理
原理就是Google在那個用來編譯的gradle插件中增加了新功能,當(dāng)某個module開啟ViewBinding功能后肠骆,編譯的時候就去掃描此模塊下的layout文件算途,生成對應(yīng)的binding類。
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
@NonNull
public final TextView tvTextView;
private ActivityMainBinding(@NonNull ConstraintLayout rootView,
@NonNull TextView rvDataList) {
this.rootView = rootView;
this.tvTextView = tvTextView;
}
@Override
@NonNull
public ConstraintLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static ActivityMainBinding 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 tvTextView = rootView.findViewById(R.id.tv_text);
if (tvTextView == null) {
missingId = "tvTextView";
break missingId;
}
return new ActivityMainBinding((ConstraintLayout) rootView, tvTextView);
}
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
可以看出蚀腿,最終使用的仍然是findViewById嘴瓤,和ButterKnife異曲同工扫外,不同的是ButterKnife通過編譯時注解生成ViewBinding類,而ViewBinding是通過編譯時掃描layout文件生成ViewBinding類廓脆。
5. 優(yōu)點(diǎn)
5.1 更早發(fā)現(xiàn)程序中部分錯誤:
發(fā)現(xiàn)程序錯誤的階段不同:findViewById發(fā)生在運(yùn)行時筛谚,ViewBinding作用在編譯期⊥7蓿可以更早期的語法和發(fā)現(xiàn)錯誤驾讲。
與使用 findViewById 相比,視圖綁定具有一些很顯著的優(yōu)點(diǎn):
Null 安全:由于視圖綁定會創(chuàng)建對視圖的直接引用席赂,因此不存在因視圖 ID 無效而引發(fā) Null 指針異常的風(fēng)險(xiǎn)吮铭。此外,如果視圖僅出現(xiàn)在布局的某些配置中(比如橫豎屏布局內(nèi)容差異)颅停,則綁定類中包含其引用的字段會使用 @Nullable 標(biāo)記谓晌。
類型安全:每個綁定類中的字段均具有與它們在 XML 文件中引用的視圖相匹配的類型。這意味著不存在發(fā)生類轉(zhuǎn)換異常的風(fēng)險(xiǎn)癞揉。
這些差異意味著布局和代碼之間的不兼容將會導(dǎo)致構(gòu)建在編譯時(而非運(yùn)行時)失敗纸肉。
5.2 使用更加方便簡潔喊熟,相同的功能柏肪,使用的編碼量更少
使用時findViewById需要提前聲明視圖,聲明過程冗余繁瑣逊移。ViewBinding直接使用预吆,簡潔明了,易于維護(hù)胳泉,更少的代碼實(shí)現(xiàn)相同的功能拐叉。
6.缺點(diǎn)
1、總代碼量并沒有減少扇商,機(jī)械化的代碼被自動工具生成凤瘦,我們可以直接使用
2、通過布局文件查找使用的地方變得相對困難案铺,需要手動輸入生成后的類名查找
3蔬芥、如果為了簡化binding對象的獲取需要用到反射實(shí)現(xiàn),影響部分性能(對現(xiàn)在市面上的手機(jī)這點(diǎn)性能應(yīng)該可以忽略不計(jì))
4控汉、如果為了取代ButterKnife笔诵,只能取代部分功能,針對ButterKnife點(diǎn)擊之類的姑子,需要轉(zhuǎn)成原始的setOnClickListner()來實(shí)現(xiàn)乎婿。用 lambda表達(dá)式的話,影響不大街佑。