视图绑定:ViewBinding
安卓开发中让开发者厌烦的要数findViewById(),早些时候JakeWharton大神写的View注入框架ButterKnife很火,干掉重复的工作提高效率。不过现在不再维护,而且用的也越来越少。
2020年3月Google官方推出了Android Studio3.6,亮点就是ViewBinding,来解决上面那个烦人的问题。ViewBinding也用了一年多,记录一下。
1、启用ViewBinding
只需在模块的build.gradle中将viewBinding属性置为true即可。
android {
//在模块build.gradle中节点的位置不要搞错,在android节点下面
viewBinding {
enabled = true
}
...
}或者:
android {
buildFeatures {
viewBinding true
}
...
}注意需要AndroidStudio版本至少3.6 Canary 11,且Android Gradle 插件版本至少3.6.1。
buildscript {
//这里版本已经是最新的4.2.1
dependencies {
classpath "com.android.tools.build:gradle:4.2.1"
}
}启用DataBinding的项目,同时也默认启用ViewBinding,另见DataBinding:开启数据绑定系列笔记。但是两者不要同时添加到build.gradle里,否则在有些场景会冲突:比如对于ViewStub,这两者的处理策略不一样,DataBinding使用ViewStubProxy代理ViewStub,而ViewBinding直接使用ViewStub,这种情况只能二选一,关掉ViewBinding即可。
2、使用ViewBinding
写个简单的布局演示一下,布局文件名为:activity_layout_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/confirm_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!--无id-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>生成的类名根据布局文件名转换而来:布局文件名一般用下划线命名法,生成的绑定类名则对应去掉下划线首字母大写并且加上Binding后缀。比如activity_layout_main.xml布局,生成的绑定类名就是ActivityLayoutMainBinding。
在Activity中使用绑定类:
//全局保存一个绑定类实例
ActivityLayoutMainBinding binding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//初始化绑定类,源码后面再看
binding = ActivityLayoutMainBinding.inflate(getLayoutInflater());
//将根View设置给Content
setContentView(binding.getRoot());
}通过绑定类可以轻松的拿到布局文件中的View控件(前提是这些在布局中的View都必须有id)。布局中控件的id一般采用下划线命名法,对应绑定类中的控件变量名。
//布局中id为cancel_button的Button控件
Button cancelBtn = binding.cancelButton;
//布局中id为confirm_button的Button控件
binding.confirmButton.setOnClickListener(v -> {/*on click*/});
//布局中id为text的Button控件
binding.text.setText("Quibbler.cn");就是这么方便,比一个个去findViewById()方便的多。但实际情况远不止这些,项目使用中会遇到各种“坑”。
3、ViewBinding原理初探 *
ViewBinding其实是Android Gradle插件在3.6.1以上增加的新功能:
classpath "com.android.tools.build:gradle:4.2.1"开启ViewBinding后,编译时会扫描layout资源文件,在/res/layout*文件夹下的布局会生成对应的Binding类(至于如何扫描解析XML、策略,这里就不得而知了)。

如果不需要让某个布局生成绑定类,只需要在布局根节点加上tools:viewBindingIgnore="true",就不会生成该布局的绑定类:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
tools:viewBindingIgnore="true">
...
</LinearLayout>生成的绑定类在build/generated/data_binding_base_class_source_out/debug/out/包名/databinding目录下:

看一下生成的绑定类代码,就会发现ViewBinding和和ButterKnife异曲同工:内部仍然是通过findViewById()查找初始化View。区别在于ButterKnife通过注解指定每个View对应的id,而ViewBinding则是直接扫描布局文件解析有id的控件。
// Generated by view binder compiler. Do not edit!
public final class ActivityLayoutMainBinding implements ViewBinding {
//布局根View
@NonNull
private final LinearLayout rootView;
//从布局文件中扫描id解析出来的控件,作为绑定类中的成员
@NonNull
public final Button cancelButton;
@NonNull
public final Button confirmButton;
@NonNull
public final IncludeLayoutBinding layout;
@NonNull
public final TextView text;
//绑定类的构造函数,有n个带id的控件,构造函数就有n+1个参数
private ActivityLayoutMainBinding(@NonNull LinearLayout rootView, @NonNull Button cancelButton,
@NonNull Button confirmButton, @NonNull IncludeLayoutBinding layout, @NonNull TextView text) {
this.rootView = rootView;
this.cancelButton = cancelButton;
this.confirmButton = confirmButton;
this.layout = layout;
this.text = text;
}
//当前布局的根View
@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}
//一般用该方法
@NonNull
public static ActivityLayoutMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
//加载对应的layout布局文件:R.layout.activity_layout_main
@NonNull
public static ActivityLayoutMainBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_layout_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
//最终通过bind方法,内部还是用findViewById一个个去查找初始化View控件
@NonNull
public static ActivityLayoutMainBinding 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.
int id;
missingId: {
id = R.id.cancel_button;
Button cancelButton = rootView.findViewById(id);
if (cancelButton == null) {
break missingId;
}
id = R.id.confirm_button;
Button confirmButton = rootView.findViewById(id);
if (confirmButton == null) {
break missingId;
}
id = R.id.layout;
View layout = rootView.findViewById(id);
if (layout == null) {
break missingId;
}
IncludeLayoutBinding binding_layout = IncludeLayoutBinding.bind(layout);
id = R.id.text;
TextView text = rootView.findViewById(id);
if (text == null) {
break missingId;
}
return new ActivityLayoutMainBinding((LinearLayout) rootView, cancelButton, confirmButton,
binding_layout, text);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}4、ViewBinding拓展 *
前面对ViewBinding的使用只是简单的介绍,善于思考的同学看到这里对ViewBinding已经有一堆疑问。因为项目布局往往很复杂,夹杂着<include/>、<merge/>标签,再比如在布局彩蛋:BlinkLayout闪烁布局一文就有用到<blink/>特殊标签。
①布局的layout数量是否和生成的绑定类文件数量一样?
②layout和layout-w200dp布局文件夹如果有相同名称的布局文件,如何生成绑定类,是覆盖还是合并两个布局呢?
③在一个layout布局中使用<include/>另一个layout布局,生成的绑定类会不会包含include的布局?
④对于更复杂的布局,使用<merge/>合并另一个布局呢,生成的绑定类是什么样?
4.1、布局和绑定类数量是否一致?
布局的数量和生成的绑定类数量没太大关系,一开始也误以为:有多少个布局文件就会生成多少个绑定类。仔细思考了一下,发现不是这么回事:
①如果只有一个layout文件夹,默认情况下,有多少个布局就会生成多少个绑定类。这时是一一对应,数量一样。
②可以在布局里用tools:viewBindingIgnore="true"声明不生成该布局的绑定类,生成的绑定类数量 < 布局数量。
③有多个layout文件夹(layout、layout-w200dp等),此时会复杂一些,涉及到布局合并,继续往下看:
4.2、多个layout资源目录
项目为了适配不同屏幕大小的设备,有多个layout文件夹,文件夹下有同名的布局文件,应用在不同的场景下自动选择合适的布局。以layout和layout-small为例,两个文件夹下有一样名称的布局文件activity_main.xml:

在layout文件夹下内容如下,简单放一个TextView:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout >
<TextView
android:id="@+id/text" />
</androidx.constraintlayout.widget.ConstraintLayout>而在layout-small文件夹下布局中控件则为Button:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout >
<Button
android:id="@+id/text" />
</androidx.constraintlayout.widget.ConstraintLayout>编译之后发现,只生成一个ActivityMainBinding类,内容是前面两个布局的合并:
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
//此控件对应layout-small目录下的activity_main.xml
@Nullable
public final Button button;
//此控件对应layout目录下的activity_main.xml
@Nullable
public final TextView text;
private ActivityMainBinding(@NonNull ConstraintLayout rootView, @Nullable Button button,
@Nullable TextView text) {
this.rootView = rootView;
this.button = button;
this.text = text;
}
@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) {
Button button = ViewBindings.findChildViewById(rootView, R.id.button);
TextView text = ViewBindings.findChildViewById(rootView, R.id.text);
return new ActivityMainBinding((ConstraintLayout) rootView, button, text);
}
}最大的区别是,这里面的控件不再是NonNull,而是Nullable。对于layout文件夹下使用的布局,其TextView是非空,而没有Button这个控件。相反,对于layout-small文件夹下使用的布局,其Button存在且非空,不存在TextView控件。使用的时候要更加小心,需要判空。
4.3、布局包含<include/>标签
在activity_main.xml布局中添加<include/>标签,
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout >
<TextView
android:id="@+id/text"/>
<include layout="@layout/include_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>包含另一个布局include_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout >
<ImageView
android:id="@+id/include_image"/>
</LinearLayout>生成的Binding类只有一个TextView,而没有被包含布局的内容:
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
@NonNull
public final TextView text;
private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull TextView text) {
this.rootView = rootView;
this.text = text;
}
...
}那怎么办呢?只需要给<include/>标签上id:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout >
<TextView
android:id="@+id/text"/>
<include
android:id="@+id/include_layout_id"
layout="@layout/include_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>重新编译,发现生成的ActivityMainBinding中新增了一个成员变量:IncludeLayoutBinding。
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
//生成IncludeLayoutBinding
@NonNull
public final IncludeLayoutBinding includeLayoutId;
@NonNull
public final TextView text;
private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull TextView text) {
this.rootView = rootView;
this.text = text;
}
...
}这个绑定类就是<include/>标签所包含的include_layout.xml布局对应的绑定类:IncludeLayoutBinding。
public final class IncludeLayoutBinding implements ViewBinding {
//<include />所包含的另一个布局对应的绑定类
@NonNull
private final LinearLayout rootView;
@NonNull
public final ImageView includeImage;
...
}可以直接通过activityMainBinding.includeLayoutId拿到<include/>标签所包含子布局进行操作:
ActivityMainBinding activityMainBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(activityMainBinding.getRoot());
//直接拿到include的布局绑定类
IncludeLayoutBinding includeLayoutBinding = activityMainBinding.includeLayoutId;
//对IncludeLayoutBinding子布局进行操作
includeLayoutBinding.includeImage.setImageResource(R.drawable.ic_launcher_foreground);4.4、布局包含<merge/>标签
<include/>复用布局会使布局层级增加,降低性能。在《布局优化 & 过度绘制检测》一文提到用<merge/>标签解决这个问题。修改一下4.3节中的include_layout.xml布局:
<?xml version="1.0" encoding="utf-8"?>
<merge>
<LinearLayout >
<ImageView
android:id="@+id/include_image" />
</LinearLayout>
</merge>可以通过编译,但是运行直接奔溃,报错:Missing required view with ID: cn.quibbler.blink:id/include_layout_id。把<include />中的id去掉,手动初始化IncludeLayoutBinding:
ActivityMainBinding activityMainBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(activityMainBinding.getRoot());
//bind()初始化IncludeLayoutBinding,而不能直接通过ActivityMainBinding拿到IncludeLayoutBinding
IncludeLayoutBinding includeLayoutBinding = IncludeLayoutBinding.bind(activityMainBinding.getRoot());
//对IncludeLayoutBinding子布局进行操作
includeLayoutBinding.includeImage.setImageResource(R.drawable.ic_launcher_background);4.5、布局中有ViewStub
除了上面提到的<include/>、<merge/>,布局还会用到ViewStub来优化性能,详见ViewStub懒加载一文。修改一下布局,添加一个ViewStub试试看:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout >
<ViewStub
android:id="@+id/view_stub"
android:layout="@layout/include_layout" />
</LinearLayout>ViewStub中引用另一个布局include_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout >
<ImageView
android:id="@+id/include_image"/>
</LinearLayout>ViewStub也继承子View,把它当成普通View使用:
ActivityMainBinding activityMainBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(activityMainBinding.getRoot());
//inflate()懒加载ViewStub中的布局
activityMainBinding.viewStub.inflate();那么加载后的布局如何操作呢?当ViewStub加载后,初始化IncludeLayoutBinding即可:
activityMainBinding.viewStub.inflate();
//一定要在ViewStub加载后,才能初始化IncludeLayoutBinding,否则报错
IncludeLayoutBinding includeLayoutBinding = IncludeLayoutBinding.bind(activityMainBinding.getRoot());
includeLayoutBinding.includeImage.setImageResource(R.drawable.ic_launcher_background);和4.4节中的<merge/>一样,当ViewBinding无法替我们自动完成全部布局的初始化,就需要开发者在合适的时候自行初始化。
相关资料:
文档 > 指南 > 视图绑定">Android 开发者 > 文档 > 指南 > 视图绑定

