嗯哼,你没有看错,ViewBinding确实是架构化组件的重要组成部分之一,主要负责视图绑定方面 ~
在Android中findViewById是最常见的模板代码之一,在我新手时期最喜欢写这样的代码,因为很有成就感…
But - 随着项目代码体量的不断变大,像findViewById这样的模板代码,注定要被优化掉,记得早期用的是ButterKnife黄油刀优化该模板代码,不过在Gradle插件升级到5.0版本之后,ButterKnife将无法再被使用!故此我们需要寻找的新的方式来解决这个问题 ~
- 基础介绍
- 简单原理
- 使用方式
- 使用场景
- Activity
- include
- include布局不包含merge标签
- include布局包含merge标签
- Fragment
- Adapter
- 自定义Dialog
- 自定义View
- 布局中不包含Merge标签
- 布局中包含Merge标签
- 俩种场景下的ViewBinding
随着Google每年的不断强大,在2019年I/O大会上公布了一款Android视图绑定工具ViewBinding。它的使用方式有点类似DataBinding,但相比DataBinding,ViewBinding是一个更轻量级、更纯粹的findViewById的替代方案。
在市场上的很多开发者出现了语言分化,一部分继续使用Java开发,另一部分则使用Google推荐的Kotlin开发
针对不同语言,Kotlin开发者使用的是2017年推出的kotlin-android-extensions插件,Java开发者使用的是Google2019年推出的 ViewBinding ~
Look here:为何现在才归纳ViewBinding?主要因为近期kotlin-android-extensions插件宣布不久就会废弃了,这意味着很难保证以后的兼容性,同时引用方式也发生了改变;所以现在很多kt开发者也在想着改变项目中视图组件为ViewBinding?
为何使用ViewBinding?它与findViewById 有什么区别?与数据绑定又有什么区别?
与 findViewById 的区别
- Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null 指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用 @Nullable 标记。
- 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。
这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。
与数据绑定的对比
视图绑定和数据绑定均会生成可用于直接引用视图的绑定类。但是,视图绑定旨在处理更简单的用例,与数据绑定相比,具有以下优势:
- 更快的编译速度:视图绑定不需要处理注释,因此编译时间更短。
- 易于使用:视图绑定不需要特别标记的 XML 布局文件,因此在应用中采用速度更快。在模块中启用视图绑定后,它会自动应用于该模块的所有布局。
反过来,与数据绑定相比,视图绑定也具有以下限制:
- 视图绑定不支持布局变量或布局表达式,因此不能用于直接在 XML 布局文件中声明动态界面内容。
- 视图绑定不支持双向数据绑定。
考虑到这些因素,在某些情况下,最好在项目中同时使用视图绑定和数据绑定。您可以在需要高级功能的布局中使用数据绑定,而在不需要高级功能的布局中使用视图绑定。
So:现在Kotlin和Java都在积极使用ViewBinding,主要还是以下优点导致的 ~
- Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null 指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用 @Nullable 标记。
- 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。
这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。 - ViewBinding生成的绑定类是一个Java类,并且添加了Kotlin的注解,可以很好的支持 Java 和 Kotlin 两种编程语言。
简单原理
ViewBinding主要是将layout内的xml进行组件实体化,也就意味着每一个xml都对应着一个ViewBinding类,那么在项目中我们可以直接通过 xxxBind类进行相关使用 ~
Activity使用示例
xxxMainBinding mBinding = xxxBinding.inflate(getLayoutInflater()); setContentView(mBinding.getRoot());
在使用前,先了解一些基本知识
- xml存放位置:常规存于layout目录
- ViewBinding存储位置:我们可以通过app - build - generated - 查看data_binding目录下的Binding类
原理解析:我们通过一个简单的 xml 和 Binding类 来分析ViewBinding的初级原理~
activity_main
对应的ActivityMainBinding
ActivityMainBinding
个人总结:内部依旧采用了findViewById的方式获取控件,不过在其之上封装了控件id为null的场景,同时进行了控件的类型匹配,减少了我们手动转换的过程 ~
// Generated by view binder compiler. Do not edit!
package com.example.viewbinding.databinding;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import com.example.viewbinding.R;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView;
@NonNull
public final TextView tvText;
private ActivityMainBinding(@NonNull LinearLayout rootView, @NonNull TextView tvText) {
this.rootView = rootView;
this.tvText = tvText;
}
@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}
//视图填充,重载方法1
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
//视图填充,重载方法2
@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);
}
//Look Here:内部封装了控件为NUll的场景,同时控件进行了类型匹配
@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.
int id;
missingId: {
id = R.id.tv_text;
TextView tvText = rootView.findViewById(id);
if (tvText == null) {
break missingId;
}
return new ActivityMainBinding((LinearLayout) rootView, tvText);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
使用方式
环境要求-注意:视图绑定在 Android Studio 3.6 Canary 11 及更高版本 中可用。
具体步骤
- build.gradle加入以下配置(支持按模块启用ViewBinding)
android {
...
viewBinding {
enabled = true
}
}
- layout中创建对应的xml
activity_main
补充:如不希望生成某xml的视图绑定类,则可在该xml的根布局加入以下属性
tools:viewBindingIgnore="true"
- 不同场景使用ViewBinding ,此处以Activity为示例
- 未使用ViewBinding
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView mText = findViewById(R.id.tv_text);
mText.setText("tvText");
}
}
- 已使用ViewBinding
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());
mBinding.tvText.setText("tvText");
}
}
使用场景
为什么要单独说一下使用场景?主要是ViewBinding在Activity、Fragment、Adapter、自定义View等场景中的使用,有所区别 ~
Activity如需设置绑定类的实例以供 Activity 使用,请在 Activity 的 onCreate() 方法中执行以下步骤:
- 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Activity 使用。
- 通过调用 getRoot()方法或使用 Kotlin 属性语法 获取对根视图的引用。
- 将根视图传递到 setContentView(),使其成为屏幕上的活动视图。
package com.example.viewbinding;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import com.example.viewbinding.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());
mBinding.tvText.setText("tvText");
}
}
activity_main
include
不论是在Activity,还是Fragment中,如果你使用了include标签,那么都首先要通过一级xml的id,之后在获取include内部xml的控件id ~
include布局不包含merge标签include_text(include视图)
activity_main(引用include的xml)
MainActivity
package com.example.viewbinding;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import com.example.viewbinding.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());
//include部分
mBinding.mainInclude.includeTvText.setText("Include Text");
mBinding.mainInclude.includeTvText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mBinding.tvText.setText("Include 点击事件");
mBinding.mainInclude.includeTvText.setText("Include 点击事件");
}
});
}
}
补充:在使用include时,不可在二级视图的根布局加id,否则ViewBinding会报错,示例如下 ~
include_text(include视图)
include布局包含merge标签
include_text(include视图)
仅将最外层布局更改为merge标签
Look here:其他与上方相同,包含xml的引用和ViewBinding的使用
在运行项目后,会直接报错:Caused by: java.lang.NullPointerException: Missing required view with ID: com.example.viewbinding:id/main_include
因为每个xml都会生成一个ViewBinding类,通过查看IncludeTextBinding后,我们将子类的xml通过bind的方式绑定在根布局上 ~
@NonNull
public static IncludeTextBinding 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.include_tv_text;
TextView includeTvText = rootView.findViewById(id);
if (includeTvText == null) {
break missingId;
}
return new IncludeTextBinding(rootView, includeTvText);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
具体操作如下,亲测可用
- 去除include标签 id
- 通过IncludeTextBinding.bind(view)的方式获取ActivityMainBinding视图,思想上有点像include常规的id查询方式
package com.example.viewbinding;
import android.os.Bundle;
import com.example.viewbinding.databinding.ActivityMainBinding;
import com.example.viewbinding.databinding.IncludeTextBinding;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());
//之前的
// mBinding.mainInclude.includeTvText.setText("Include Text");
IncludeTextBinding includeBinding = IncludeTextBinding.bind(mBinding.getRoot());
includeBinding.includeTvText.setText("Include 冲刺把,卡布达");
}
}
Fragment
如需设置绑定类的实例以供 Fragment 使用,请在 Fragment 的 onCreateView() 方法中执行以下步骤:
- 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Fragment 使用。
- 通过调用 getRoot() 方法或使用 Kotlin 属性语法 获取对根视图的引用。
- 从 onCreateView() 方法返回根视图,使其成为屏幕上的活动视图。
注意:注意:inflate() 方法会要求您传入布局膨胀器。如果布局已膨胀,您可以调用绑定类的静态 bind() 方法。如需了解详情,请查看视图绑定 GitHub 示例中的例子。
package com.example.viewbinding;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.example.viewbinding.databinding.FragmentTestBinding;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class TestFragment extends Fragment {
private FragmentTestBinding mBinding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentTestBinding.inflate(inflater);
mBinding.tvFragment.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mBinding.tvFragment.setText("冲击吧,卡布达~");
}
});
return mBinding.getRoot();
}
//官方建议:防止OOM(感觉在项目中可以通过Lifecycle写个baseFragment)
@Override
public void onDestroy() {
super.onDestroy();
mBinding = null;
}
}
fragment_test
Adapter
此处借鉴了一篇 blog 中的使用方式,需注意以下要点
- ViewHolder的构造器参数改为使用的Binding对象
- 实例化ViewHolder的时候传入相应的Binding对象
package com.example.viewbinding; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.example.viewbinding.databinding.ItemAdapterBinding; import java.util.List; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; class TestAdapter extends RecyclerView.Adapter{ private List mList; public TestAdapter(List list) { mList = list; } //LookHere:不同的就在这里 @NonNull @Override public TestHolder 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的写法 ItemAdapterBinding testBinding = ItemAdapterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); TestHolder testHolder = new TestHolder(testBinding); return testHolder; } @Override public void onBindViewHolder(@NonNull TestHolder holder, int position) { holder.mItem.setText(""); } @Override public int getItemCount() { return mList.size(); } //LookHere:不同的就在这里 public class TestHolder extends RecyclerView.ViewHolder { TextView mItem; //常规写法 // public TestHolder(@NonNull View itemView) { // super(itemView); // mItem = itemView.findViewById(R.id.tv_item); // } //使用ViewBinding的写法 TestHolder(@Nullable ItemAdapterBinding itemAdapterBinding) { super(itemAdapterBinding.getRoot()); mItem = itemAdapterBinding.tvItem; } } }
item_adapter
自定义Dialog
关于自定义Dialog的写法与Activity、Fragment相近,不需特别关照
package com.example.viewbinding;
import android.app.Dialog;
import android.content.Context;
import android.view.View;
import com.example.viewbinding.databinding.DialogTestBinding;
import androidx.annotation.NonNull;
public class TextDialog extends Dialog {
private View mView;
public TextDialog(@NonNull Context context) {
super(context);
}
public TextDialog(@NonNull Context context, int themeResId) {
super(context, themeResId);
//常规写法
//mView = View.inflate(getContext(), getLayoutId(), null);
//使用ViewBinding的写法
DialogTestBinding mBinding = DialogTestBinding.inflate(getLayoutInflater());
mView = mBinding.getRoot();
setContentView(mView);
}
}
dialog_test
自定义View
关于自定义View的相关部分主要借鉴了此篇 blog ,根据layout是否使用merge?划分为俩部分进行讲解 ~
布局中不包含Merge标签view_my_layout(子布局)
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);
}
//Notice:视图异常,布局无法填充满
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());
}
}
布局中包含Merge标签
view_my_layout_merge(子布局)
MyLinearLayout(自定义控件)
Look here:仅init20()使用ViewBinding有效
private void init20() {
ViewMyLayoutMergeBinding binding = ViewMyLayoutMergeBinding.inflate(LayoutInflater.from(getContext()), this);
}
//Notice:没有效果,可以理解为还没有rootView
private void init21() {
ViewMyLayoutMergeBinding binding = ViewMyLayoutMergeBinding.bind(this);
}
俩种场景下的ViewBinding
我们对比下使用merge标签和不使用merge标签所对应的Binding文件
- 不使用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);
}
- 使用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);
}



