赞
踩
沉舟侧畔千帆过,
病树前头万木春。
– 唐·刘禹锡
随着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版本回退了,一切都回归平静。【如果有解决办法的请告知我,感激不尽】
它和ButterKnife一样都是为了省去findViewById()这样的重复代码。其实在2019谷歌开发者峰会上对ViewBinding就已经有所耳闻了,layout中更新控件ID后立刻可以在Activity中引用到,这绝对比ButterKnife需要编译、需要区分R和R2要舒服的多。
上面升级到3.6.0就是为了使用它,然而现实永远这么的残酷,十之八九不尽人意,ViewBinding和ButterKnife看来只能二选一了。
关于ViewBinding的文档,官方写的很详细,请看 视图绑定 。本文一切从简,主要说下Google官方没有提到的一些问题。
ViewBinding支持按模块启用,在模块的build.gradle文件中添加如下代码:
android {
...
viewBinding {
enabled = true
}
}
//之前设置视图的方法
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中直接使用就可以。
当我们在布局中添加一个id为 tv_text 的TextView后,直接在Activity中使用mBinding.tvText即可拿到该控件。如下所示,可以看到也是以控件ID的驼峰命名法来获取的:
mBinding.tvText.setText("是你得不到的ViewBinding");
例如我们有个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中代码如下:
mBinding.layoutInclude.tvInclude.setText("这就是你的不对了");
是不是很神奇,是不是很简单。
注意:
当你给layout_comment.xml的根布局再添加id(比如添加了layout_xxx的ID)的时候,此时会报错:
java.lang.NullPointerException: Missing required view with ID: layout_xxx
我们将上文的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代码中依旧是可以正常使用以下代码的:
mBinding.layoutInclude.tvInclude.setText("会不会出现问题呀");
但是但是!!!运行就会报错:
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
在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的对象后,更新数据的都和之前一样了。
请注意官方的提示( 务必在onDestroyView() 方法中清除对绑定类实例的所有引用),感谢 @lingHui_1314 的反馈:
注意:Fragment 的存在时间比其视图长。请务必在 Fragment 的 onDestroyView() 方法中清除对绑定类实例的所有引用。
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); } }
我在重构工程的时候发现了自定义视图中其实有很多问题,这里把这两种常见的方法总结下:
这里直接贴出来代码吧,就是自定义了一个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()); } }
我们添加一个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方法之后,下面的文章其实可以没必要看了。
在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; } } }
只需要注意两方面:
大概了解了ViewBinding后,我们可以考虑将其完全封装在BaseActivity(BaseFragment、BaseDialog、BaseView等)等底层的公共类中,省去手动实例化相应ViewBinding类的这一过程。
首先可以使用泛型类,每个具体的Activity继承BaseActivity,并传递进去对应的ViewBinding,然后反射对应的inflate()方法获取到ViewBinding实例。拿到实例后就可以对控件为所欲为了不是!!
使用ViewBinding的话,其实很简单,新建xxx.xml布局后就会产生一个对应的 xxxBinding.java的文件,实例化xxxBinding只需要调用它自身的inflate()方法即可。
注意不同情况下使用不同的inflate()方法,以及使用了merge标签情况下的bind()方法,以及使用merge标签布局和其他正常xxxLayout布局所产生的不同的inflate()方法。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。