一、控件的声明
在Activity中绑定布局中的控件一般有三种实现方式:
- 第一种用最原生态的findViewById方法来绑定
- 第二种方式可以使用ButterKnife开源框架实现,ButterKnife对组件化的支持却很不友好
- 第三种方式是使用Kotlin的扩展插件来获取视图控件,使用局限性:无法跨模块操作,类型不安全:不同的资源文件可以存在相同的控件id,因此在View层存在引用id来源出错的问题。
Kotlin 1.4版本中废弃了扩展插件,Google推荐使用ViewBinding来替代废弃的扩展插件
二、ViewBinding的基本使用
build.gradle中添加如下配置:
android {
...
viewBinding {
enabled = true
}
...
}
配置完成后,系统会为该模块中的每个XML布局文件生成一个绑定类,这个绑定类的命名就是XML文件的名称转换为驼峰式,并在末尾添加“Binding”一词,直接可以通过view binding这一中间类获取到xml中定义的view组件了
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_new_stock_bjs_records);
viewBinding = ActivityNewStockBjsRecordsBinding.inflate(getLayoutInflater());
viewBinding.navigationBar.setText("北交所新股申购")
}
需要注意的一点是,开发者需要在onDestroyView()方法中将绑定类实例赋值为null。Fragment的存在时间比其视图时间长,所以开发者需要在onDestroyView()方法中清除对绑定类实例的所有引用,否则可能存在内存泄漏的风险
注意
如果某个布局文件不需要的话,可以通过tools:viewBinding-Ignore=“true”属性来设置
三、ViewBinding特点
- 集成简单
- 代码简洁,维护容易
- 对象空值安全:由于视图绑定会对视图直接引用,因此不存在因视图id无效而引发空指针异常的风险。
- 类型安全:每个绑定类中的字段均具有与它们在xml文件中引用的视图相匹配的类型,因此不存在强制转换可能导致的异常问题
四、ViewBinding的封装
ViewBinding组件的使用流程基本是固定的,主要分为三步:
- 调用生成的绑定类中的inflate()方法来获取绑定类的实例。
- 通过调用绑定类的getRoot()方法获取对根视图。
- 将根视图传递到setContentView()中,并与当前Activity绑定。
由于ViewBinding使用的流程是固定的,因此在基础业务的开发中,经常会定义一个BaseActivity处理所有Activity的相同业务逻辑,这时,就可以将这部分逻辑封装在BaseActivity中
/**
* 使用ViewBinding的基础类
*
* @param <T>
*/
public abstract class ViewBindingBaseActivity<T extends ViewBinding> extends ActivityBase {
protected T mViewBinding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewBinding = getViewBinding();
setContentView(mViewBinding.getRoot());
}
/**
* @return 返回Activity对于xml生成的ViewBinding对象
*/
public abstract T getViewBinding();
}
五、源码
在项目模块的build.gradle中开启ViewBinding功能之后,若进行项目编译,就会扫描layout下所有的布局文件,并生成对应的绑定类(Project视图:
app/build/generated/data_binding_base_class_source_out/debug/out/包名/databinding路径下)。这一点是由gradle插件实现的
public final class ActivityTestBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView;
@NonNull
public final ImageView imShezhi;
@NonNull
public final HomeRefresh reRefresh;
@NonNull
public final RecyclerView rv;
@NonNull
public final TextView tvName;
private ActivityTestBinding(@NonNull LinearLayout rootView, @NonNull ImageView imShezhi,
@NonNull HomeRefresh reRefresh, @NonNull RecyclerView rv, @NonNull TextView tvName) {
this.rootView = rootView;
this.imShezhi = imShezhi;
this.reRefresh = reRefresh;
this.rv = rv;
this.tvName = tvName;
}
@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityTestBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityTestBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_test, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static ActivityTestBinding 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: {
ImageView imShezhi = rootView.findViewById(R.id.im_shezhi);
if (imShezhi == null) {
missingId = "imShezhi";
break missingId;
}
HomeRefresh reRefresh = rootView.findViewById(R.id.re_refresh);
if (reRefresh == null) {
missingId = "reRefresh";
break missingId;
}
RecyclerView rv = rootView.findViewById(R.id.rv);
if (rv == null) {
missingId = "rv";
break missingId;
}
TextView tvName = rootView.findViewById(R.id.tv_name);
if (tvName == null) {
missingId = "tvName";
break missingId;
}
return new ActivityTestBinding((LinearLayout) rootView, imShezhi, reRefresh, rv, tvName);
}
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
从生成的ActivityTestBinding文件中可以轻松地看出,在调用了inflate之后会调用bind方法,而bind方法依然是通过findViewById绑定