Android ViewBinding入门

前言

随着Android Studio 3.6的正式发布,Gradle插件升级到3.6.0,ButterKnife报错,日志如下:

Attempt to use @BindView for an already bound ID 0 on 'mTvText'.

回退Gradle版本后回归正常,事实上,ButterKnife作者已不推荐使用ButterKnife,取而代之的是ViewBinding

初识ViewBinding

ViewBinding文档:https://developer.android.google.cn/topic/libraries/view-binding

ViewBinding和ButterKnife一样都是为了省去findViewById()这样的重复代码。其实在2019谷歌开发者峰会上对ViewBinding就已经有所耳闻,layout中更新控件ID后立刻可以在Activity中引用到,这绝对比ButterKnife需要编译、需要区分R和R2要舒服的多。

使用ViewBinding

环境要求

  • Android Studio版本3.6及以上
  • Gradle 插件版本3.6.0及以上

开启ViewBinding功能

ViewBinding支持按模块启用,在模块的build.gradle文件中添加如下代码:

android {
    ...
    viewBinding {
        enabled = true
    }
}    

Activity中ViewBinding的使用

//之前设置视图的方法
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测试");

布局中使用include

例如我们有个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="include测试"
        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("ViewBinding include测试");

是不是很神奇,是不是很简单

注意:当给layout_comment.xml的根布局再添加id(比如添加了layout_xxx的ID)的时候,此时会报错:

java.lang.NullPointerException: Missing required view with ID: layout_xxx

布局中使用include和merge

我们将上文的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="merge测试"
        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("ViewBinding merge测试");

但是但是!!!运行就会报错:

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("ViewBinding merge测试");

LayoutCommentBinding commentBinding = LayoutCommentBinding.bind(mBinding.getRoot());
commentBinding.tvInclude.setText("ViewBinding merge测试");

同时需要注意:include标签不可以有id

Fragment中使用ViewBinding

在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的对象后,更新数据的都和之前一样。

自定义Dialog中使用ViewBinding

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);
    }
}

自定义View中使用ViewBinding

在重构工程的时候发现自定义视图中其实有很多问题,这里把这两种常见的方法总结下:

使用的layout文件不包含merge

这里直接贴出来代码吧,就是自定义了一个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());
    }
}

使用的layout文件根标签为merge

我们添加一个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源代码,一眼就能明了我们之前的写法对应的是现在的哪个方法。

Adapter中使用ViewBinding

在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;
        }
    }
}

只需要注意两方面:

  • ViewHolder的构造器参数改为使用的Binding对象
  • 实例化ViewHolder的时候传入相应的Binding对象

关于封装

大概了解了ViewBinding后,我们可以考虑将其完全封装在BaseActivity(BaseFragment、BaseDialog、BaseView等)等底层的公共类中,省去手动实例化相应ViewBinding类的这一过程。

首先可以使用泛型类,每个具体的Activity继承BaseActivity,并传递进去对应的ViewBinding,然后反射对应的inflate()方法获取到ViewBinding实例。拿到实例后就可以对控件为所欲为了不是!!

总结

使用ViewBinding的话,其实很简单,新建xxx.xml布局后就会产生一个对应的xxxBinding.java的文件,实例化xxxBinding只需要调用它自身的inflate()方法即可。
注意不同情况下使用不同的inflate()方法,以及使用了merge标签情况下的bind()方法,以及使用merge标签布局和其他正常xxxLayout布局所产生的不同的inflate()方法。

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/25/getting-started-with-android-viewbinding/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Android ViewBinding入门
前言 随着Android Studio 3.6的正式发布,Gradle插件升级到3.6.0,ButterKnife报错,日志如下: Attempt to use @BindView for an already bound ID 0 on ……
<<上一篇
下一篇>>
文章目录
关闭
目 录