Android ViewBinding总结

4,755 阅读8分钟

 注意,本篇笔记根据Android开发者网站视图绑定部分学习总结,点击这里进入官网

作用

 通过视图绑定的功能,可以更轻松地编写可与视图交互的功能,在模块中启用视图绑定之后,系统会为该模块中的每个XML布局文件生成一个绑定类,绑定类的实例包含对在相应布局中具有ID的所有视图的直接引用。

 在大多数情况下,视图绑定会代替findViewById()

 视图绑定功能可按模块启用,要在某个模块中启用视图绑定功能,需要将viewBinding元素添加到其build.gradle文件中,如:

android{
    ···
    viewbinding{
        enabled = true
    }
}

创建一个TestViewBindingActivity

 在上面我们创建了一个TestViewBindingActivity,同时系统会自动创建出这个Activity对应的布局文件activity_test_view_binding,下面是这个布局文件的内容:

<?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=".ui.TestViewBindingActivity">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="视图绑定页面演示"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:paddingVertical="20dp"
        />

    <Button
        android:id="@+id/btn_change_title"
        app:layout_constraintTop_toBottomOf="@id/tv_title"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="修改标题"
        />
    
    <Button
        style="@style/matchWidth"
        app:layout_constraintTop_toBottomOf="@id/btn_change_title"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:text="没有什么用"
        />


</androidx.constraintlayout.widget.ConstraintLayout>

 在上面的布局文件中,创建了一个TextView和两个Button,其中TextView和第一个Button是有id的,第二个Button是没有id的。由于我们启用了viewBinding功能,所以系统会我们自动创建当前布局文件对应的视图绑定文件,也就是ActivityTestViewBindingBinding.java文件,这个文件位于/build/generated/data_binding_base_class_source_out目录下,下面是这个类中生成的全部源码:

// Generated by view binder compiler. Do not edit!
package com.project.myproject.databinding;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.viewbinding.ViewBinding;
import com.project.myproject.R;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;

public final class ActivityTestViewBindingBinding implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;

  @NonNull
  public final Button btnChangeTitle;

  @NonNull
  public final TextView tvTitle;

  private ActivityTestViewBindingBinding(@NonNull ConstraintLayout rootView,
      @NonNull Button btnChangeTitle, @NonNull TextView tvTitle) {
    this.rootView = rootView;
    this.btnChangeTitle = btnChangeTitle;
    this.tvTitle = tvTitle;
  }

  @Override
  @NonNull
  public ConstraintLayout getRoot() {
    return rootView;
  }

  @NonNull
  public static ActivityTestViewBindingBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static ActivityTestViewBindingBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.activity_test_view_binding, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static ActivityTestViewBindingBinding 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: {
      Button btnChangeTitle = rootView.findViewById(R.id.btn_change_title);
      if (btnChangeTitle == null) {
        missingId = "btnChangeTitle";
        break missingId;
      }
      TextView tvTitle = rootView.findViewById(R.id.tv_title);
      if (tvTitle == null) {
        missingId = "tvTitle";
        break missingId;
      }
      return new ActivityTestViewBindingBinding((ConstraintLayout) rootView, btnChangeTitle,
          tvTitle);
    }
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

 首先可以看到,在这个类中有三个字段,一个是我们的根布局ConstraintLayout对应的rootView字段,一个是TextView对应的tvTitle,最后一个就是Button对应的btnChangeTitle,字段名都是根据我们设置的id来取的.

 接下来是私有的构造函数,由于字段都是使用final修饰的,所以在构造函数中需要对属性进行赋值,同时我们也能知道,我们无法直接使用这个构造函数。

 接下来通过getRoot()方法返回了rootView属性,在这里我们也就可以拿到我们的xml中的根布局了。

 接下来是一个inflate()方法,需要传入一个LayoutInflater对象,方法中也只是调用了它的重载方法inflate(LayoutInflater inflater,parent null, attachToParent false)

 包含三个参数的inflate()方法:

  @NonNull
  public static ActivityTestViewBindingBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.activity_test_view_binding, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

 这个方法首先就跟我们平时加载一个View是一样的,将指定的xml文件进行加载,最后执行bind(View)方法。

 查看bind(View)方法可以发现,里面编写代码的方式和我们平时编写代码的方式不一样,查看注释可以知道:该方法的主体以原本不会编写的方式生成,这样做是为了优化已编译的字节码的大小和性能。

  @NonNull
  public static ActivityTestViewBindingBinding 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: {
      Button btnChangeTitle = rootView.findViewById(R.id.btn_change_title);
      if (btnChangeTitle == null) {
        missingId = "btnChangeTitle";
        break missingId;
      }
      TextView tvTitle = rootView.findViewById(R.id.tv_title);
      if (tvTitle == null) {
        missingId = "tvTitle";
        break missingId;
      }
      return new ActivityTestViewBindingBinding((ConstraintLayout) rootView, btnChangeTitle,
          tvTitle);
    }
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }

 其实这个方法的主体就是一个判断,主要目的就是为了判断我们之前定义的那些变量在指定的布局文件中是否能够找到,如果出现一个没找到,就抛出异常,如果找到了,就在这里调用构造函数生成相应的视图绑定的对象。

 上面我们就简单地分析了一个视图绑定文件的内容,其实还有一些方面没有演示。

include标签

 如果在xml文件中使用了include标签是怎样呢,下面是一个演示:

 首先创建了一个布局文件layout_view_binding_include.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="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        style="@style/matchWidth"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:text="include中的Text"
        android:gravity="center"
        android:padding="10dp"
        />
    
</androidx.constraintlayout.widget.ConstraintLayout>

 然后在之前的布局文件activity_test_view_binding.xml中引用:

<?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=".ui.TestViewBindingActivity">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="视图绑定页面演示"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:paddingVertical="20dp"
        />

    <Button
        android:id="@+id/btn_change_title"
        app:layout_constraintTop_toBottomOf="@id/tv_title"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="修改标题"
        />

    <Button
        style="@style/matchWidth"
        app:layout_constraintTop_toBottomOf="@id/btn_change_title"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:text="没有什么用"
        />
    
    <include
        layout="@layout/layout_view_binding_include"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        style="@style/matchWidth"
        />
</androidx.constraintlayout.widget.ConstraintLayout>

 查看/build/generated/data_binding_base_class_source_out目录,会发现多了一个LayoutViewBindingIncludeBinding.java文件,就是之前我们创建的layout_view_binding_include.xml生成的视图绑定文件。

 另外,虽然我们将上面的文件添加到通过include添加到activity_test_view_binding.xml中,但是我们并没有给include标签设置id,所以在Activity中我们还是不同通过ActivityTestViewBindingBinding对象获取include标签对应的布局。

 然后我们给include标签设置id,android:id="@+id/layout_include",接着再查看ActivityTestViewBindingBinding中的代码(rebuild一下):

package com.project.myproject.databinding;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.viewbinding.ViewBinding;
import com.project.myproject.R;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;

public final class ActivityTestViewBindingBinding implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;

  @NonNull
  public final Button btnChangeTitle;

  @NonNull
  public final LayoutViewBindingIncludeBinding layoutInclude;

  @NonNull
  public final TextView tvTitle;

  private ActivityTestViewBindingBinding(@NonNull ConstraintLayout rootView,
      @NonNull Button btnChangeTitle, @NonNull LayoutViewBindingIncludeBinding layoutInclude,
      @NonNull TextView tvTitle) {
    this.rootView = rootView;
    this.btnChangeTitle = btnChangeTitle;
    this.layoutInclude = layoutInclude;
    this.tvTitle = tvTitle;
  }

  @Override
  @NonNull
  public ConstraintLayout getRoot() {
    return rootView;
  }

  @NonNull
  public static ActivityTestViewBindingBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static ActivityTestViewBindingBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.activity_test_view_binding, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static ActivityTestViewBindingBinding 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: {
      Button btnChangeTitle = rootView.findViewById(R.id.btn_change_title);
      if (btnChangeTitle == null) {
        missingId = "btnChangeTitle";
        break missingId;
      }
      View layoutInclude = rootView.findViewById(R.id.layout_include);
      if (layoutInclude == null) {
        missingId = "layoutInclude";
        break missingId;
      }
      LayoutViewBindingIncludeBinding layoutIncludeBinding = LayoutViewBindingIncludeBinding.bind(layoutInclude);
      TextView tvTitle = rootView.findViewById(R.id.tv_title);
      if (tvTitle == null) {
        missingId = "tvTitle";
        break missingId;
      }
      return new ActivityTestViewBindingBinding((ConstraintLayout) rootView, btnChangeTitle,
          layoutIncludeBinding, tvTitle);
    }
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

 可以发现,在各个地方都加上了我们刚才设置的include标签对应的布局内容,属性名为layoutInclude(这是我们设置的id)对应的类为LayoutViewBindingIncludeBinding,也就是我们刚才创建完layout_view_binding_include.xml后自动生成的java文件。

 然后我们就可以在Activity中通过ViewBinding引用include中的布局中内容,比如对上面的include布局中的TextView设置新的文本:

binding.layoutInclude.tvInclude.text = "我也被修改了"

ViewStub标签

include标签指定的布局文件一直都存在,那么ViewStub标签呢,下面是一个演示示例,为了方便起见,我们仍然使用之前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=".ui.TestViewBindingActivity">
    
    ···

    <ViewStub
        android:id="@+id/layout_view_stub"
        app:layout_constraintTop_toBottomOf="@id/btn_change_title"
        app:layout_constraintBottom_toTopOf="@id/layout_include"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        style="@style/matchWidth"
        android:layout="@layout/layout_view_binding_include"
        />
        
    ···

</androidx.constraintlayout.widget.ConstraintLayout>

rebuild项目之后,可以发现,在ActivityTestViewBindingBinding.java文件中又多了一个属性,其类型为ViewStub,属性名为layoutViewStub:

public final class ActivityTestViewBindingBinding implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;

  @NonNull
  public final Button btnChangeTitle;

  @NonNull
  public final LayoutViewBindingIncludeBinding layoutInclude;

  @NonNull
  public final ViewStub layoutViewStub;

  @NonNull
  public final TextView tvTitle;

  private ActivityTestViewBindingBinding(@NonNull ConstraintLayout rootView,
      @NonNull Button btnChangeTitle, @NonNull LayoutViewBindingIncludeBinding layoutInclude,
      @NonNull ViewStub layoutViewStub, @NonNull TextView tvTitle) {
 ···
    this.layoutViewStub = layoutViewStub;
    this.tvTitle = tvTitle;
  }


    ···


  @NonNull
  public static ActivityTestViewBindingBinding 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: {
    
    ···
      ViewStub layoutViewStub = rootView.findViewById(R.id.layout_view_stub);
      if (layoutViewStub == null) {
        missingId = "layoutViewStub";
        break missingId;
      }
     
     ···
      return new ActivityTestViewBindingBinding((ConstraintLayout) rootView, btnChangeTitle,
          layoutIncludeBinding, layoutViewStub, tvTitle);
    }
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

 这样,我们就可以在Activity中使用ViewStub指定的布局了:

binding.layoutViewStub.setOnInflateListener(ViewStub.OnInflateListener { stub, inflated ->
    run {
        viewStubBinding = LayoutViewBindingIncludeBinding.bind(inflated)
        viewStubBinding.tvInclude.text = "我都被修改了"
    }
})

binding.layoutViewStub.inflate()

 需要注意的是,includeViewStub是不一样的,虽然在这里我们引用了同一个布局,但是include对应的类型就是指定的布局生成的视图绑定类,而ViewStub对应的类型是ViewStub,这也就直接导致在Activity中不能对ViewStub引用的布局直接进行修改,这是由于ViewStub的特性决定的。

如果不想用

 如果不想对某一个布局使用ViewBinding,那么可以在那个布局的根视图中添加tools:viewBindingIgnore="true"属性,如:

<?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="wrap_content"
    xmlns:tools="http://schemas.android.com/tools"
    tools:viewBindingIgnore="true"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/tv_include"
        style="@style/matchWidth"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:text="include中的Text"
        android:gravity="center"
        android:padding="10dp"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

 添加完这个属性之后,rebuild项目,就会发现之前生成的LayoutViewBindingIncludeBinding.java文件被删除了,同时对这个文件有引用的其他文件也会报错。