ViewBinding

291 阅读10分钟

简介

官方链接:

在大多数情况下,ViewBinding可以替代 findViewById。每当需要inflate布局时,都可以使用绑定类,例如FragmentActivity,甚至是 RecyclerView Adapter(或ViewHolder)。

启用ViewBinding

ViewBindingAndroid Studio 3.6 Canary 11 及更高版本中可用。

不需要包含任何额外的库来启用ViewBinding,从 Android Studio 3.6 中发布的版本开始,它就内置于 Android Gradle 插件中。Android Gradle Plugin 3.6 的启用方式:

android {    
    viewBinding {
        enabled = true
    }
}

在 Android Studio 4.0 中,ViewBinding已移至buildFeatures,您应该使用这种方式。Android Gradle Plugin 4.0+ 的启用方式:

android {
    buildFeatures {
        viewBinding true
    }
}

用法

为某个模块启用ViewBinding功能后,系统会为该模块中的每个XML布局文件生成一个绑定类。每个绑定类均包含对根视图以及所有具有 ID 的视图的引用。假设有一个布局文件result_profile.xml,如下:

<LinearLayout ... >
    <TextView android:id="@+id/name" />
    <ImageView android:cropToPadding="true" />
    <Button android:id="@+id/button" />
</LinearLayout>    

系统会自动生成绑定类:ResultProfileBinding,此类具有两个属性:一个是名为 nameTextView,另一个是名为 buttonButton。该布局中的 ImageView 没有 ID,因此绑定类中不存在对它的引用。

每个绑定类还包含一个 getRoot() 方法,返回布局的根视图,在此示例中,ResultProfileBinding 类中的 getRoot() 方法会返回 LinearLayout

Activity中使用ViewBinding

private val binding: ResultProfileBinding by lazy { ResultProfileBinding.inflate(layoutInflater) }

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(binding.root)
}

您现在即可使用该绑定类的实例来引用任何视图:

binding.name.text = viewModel.name
binding.button.setOnClickListener { viewModel.userClicked() }    

注:容易犯的错误:在Activity中,使用了ViewBinding,但是在setContentView()的调用中依旧传的是布局id,这会导致布局被inflate两次,且使用时使用的是binding上的对象,与界面上正在显示的不是同一个对象,所以请确保调用了setContentView(binding.root)

Fragment中使用ViewBinding

    private var _binding: ResultProfileBinding? = null
    // 这个属性只在 onCreateView 和 onDestroyView 之间有效。
    private val binding get() = _binding!!

	override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = ResultProfileBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }    

注意:Fragment 的存在时间比其视图长。请务必在 FragmentonDestroyView() 方法中清除对绑定类实例的所有引用。

绑定已经inflate的布局

对于已经inflate的布局,可以使用绑定类的静态bind()方法进行绑定。示例如下:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    val view = inflater.inflate(R.layout.result_profile, container)
    _binding = ResultProfileBinding.bind(view)
    return binding.root
}

这个方式特别适用于在Fragment中已经inflate布局的情况,如下:

创建一个新项目,并启用ViewBinding,MainActivity如下:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

activity_main.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
    	android:id="@+id/my_fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="cn.android666.myfragment.MyFragment" />

</FrameLayout>

注:FragmentContainerView标签中必须要有一个id,否则运行时会崩溃。

我们直接在布局中嵌入了FragmentFragment的代码也很简单,如下:

class MyFragment : Fragment(R.layout.my_fragment) {

}

my_fragment.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/infoText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="MyFragment"
        android:layout_gravity="center"/>

</FrameLayout>

此时运行项目是OK的,可以看到MyFagment的界面内容,此时的MyFagment布局由系统框架帮我们inflate了,所以,如果我们想使用绑定类来简化findViewById,则可以使用绑定类的bind()方法,如下:

class MyFragment : Fragment(R.layout.my_fragment) {

    private var _binding: MyFragmentBinding? = null
    private val binding get() = _binding!!

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        _binding = MyFragmentBinding.bind(view)
        binding.infoText.text = "Hello Fragment"
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
    
}

ViewBinding生成的代码

使用findViewById<TextView>(R.id.image)容易出错,比如填错了id,或者填错了转换的类型,这都会导致程序崩溃。而使用ViewBinding就不会有这样的问题,而且,在XML中编辑内容后,Android Studio会立即更新与该XML对应的绑定类,可以立即在代码中使用该绑定类,而不必等待完全重建。

下面来看一下ViewBinding生成的代码,以便了解其原理,假设有如下布局(activity_main.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 。。。>
    <TextView android:id="@+id/nameText" 。。。/>
    <Button android:id="@+id/loginButton" 。。。/>
</LinearLayout>

注:此时AndroidStudio会在内存中生成对应的绑定类:ActivityMainBinding,可以直接在代码中使用,生成的绑定类在如下路径:
在这里插入图片描述
需要注意的是:修改xml布局内容后,绑定对象在内存中会立即更新的,可以立即在代码中使用,但是如果你查看此处的这个文件,会发现里面的内容并不是即时更新的,因为我们说了,即时的更新发生在内存,如果要查看最新的对应绑定类,需要在编译运行程序后再查看才是最新的,最新内容如下:

public final class ActivityMainBinding implements ViewBinding {
  	@NonNull
  	private final LinearLayout rootView;
  	@NonNull
  	public final Button loginButton;
  	@NonNull
  	public final TextView nameText;

  	private ActivityMainBinding(@NonNull LinearLayout rootView, @NonNull Button loginButton, @NonNull TextView nameText) {
    	this.rootView = rootView;
    	this.loginButton = loginButton;
    	this.nameText = nameText;
  	}

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

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

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

  	@NonNull
  	public static ActivityMainBinding bind(@NonNull View rootView) {
    	// 这个方法体是以您不会编写的方式生成的。这样做是为了优化已编译字节码的大小和性能。
    	int id;
    	missingId: {
	      	id = R.id.loginButton;
	      	Button loginButton = ViewBindings.findChildViewById(rootView, id);
	      	if (loginButton == null) {
	        	break missingId;
	      	}
	
	      	id = R.id.nameText;
	      	TextView nameText = ViewBindings.findChildViewById(rootView, id);
	      	if (nameText == null) {
	        	break missingId;
	      	}
	
	      	return new ActivityMainBinding((LinearLayout) rootView, loginButton, nameText);
    	}
    	
    	String missingId = rootView.getResources().getResourceName(id);
    	throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  	}
}

对于rootView属性,它有一个对应的getRoot()函数来公开它。

调用bind是魔法发生的地方。它将采用inflate的布局并绑定所有属性,并添加一些错误检查以生成可读的错误消息。该bind方法使用带标签的中断来优化字节码。查看Jake Wharton 的这篇文章,了解更多关于应用优化的信息。

忽略某个布局文件

如果您希望在生成绑定类时忽略某个布局文件,请将 tools:viewBindingIgnore="true" 属性添加到相应布局文件的根视图中:

<LinearLayout 
	。。。
	tools:viewBindingIgnore="true" >

</LinearLayout>    

为不同的配置提供提示

当您在多个配置上声明视图时,有时根据特定布局使用不同的视图类型是有意义的。例如:

# in res/layout/example.xml
<TextView android:id="@+id/user_bio" />

# in res/layout-land/example.xml
<EditText android:id="@+id/user_bio" />

在这种情况下,您可能希望生成类暴露一个类型为TextViewuserBio字段,因为TextView是它们的公共基类。不幸的是,由于技术限制,视图绑定代码生成器无法做出此决定,而是简单的生成一个View类型的字段来代替,在代码中使用时,我们需要使用强制转换:binding.userBio as TextView

为了解决这个限制,视图绑定支持一个tools:viewBindingType 属性,允许你告诉编译器在生成的代码中使用什么类型。在上面的示例中,您可以使用此属性使编译器将字段生成为TextView类型:

# in res/layout/example.xml (unchanged)
<TextView android:id="@+id/user_bio" />

# in res/layout-land/example.xml
<EditText android:id="@+id/user_bio" tools:viewBindingType="TextView" />

这里仅仅在横屏的布局里设置了tools:viewBindingTypeTextView,另一个布局的不用设置,因为另一个布局中的类型已经是TextView了。假设两个布局中的类不是父子关系,但是他们有共同的父类,如果使用父类型即可,则需要在两个布局中都设置tools:viewBindingType,比如你有两个布局,一个包含BottomNavigationView,另一个包含NavigationRailView,这两个类都继承自NavigationBarViewNavigationBarView包含了大部分实现细节。如果你的代码不需要知道关于子类的细节,则可以在两种布局中使用tools:viewBindingType将生成的类型设置为NavigationBarView的类型:

# in res/layout/navigation_example.xml
<BottomNavigationView android:id="@+id/navigation" tools:viewBindingType="NavigationBarView" />

# in res/layout-w720/navigation_example.xml
<NavigationRailView android:id="@+id/navigation" tools:viewBindingType="NavigationBarView" />

请注意,视图绑定在生成代码时无法验证此属性的值。为避免编译时和运行时错误,该值必须满足以下条件:

  • 该值必须是继承自android.view.View的类。

  • 该值必须是它所在标签的超类。例如,以下值不起作用:

    <TextView tools:viewBindingType="ImageView" /> <!-- ImageView 与 TextView 没有继承关系 --> 
    <TextView tools:viewBindingType="Button" /> <!-- Button 不是 TextView 的父类 -->
    
  • 最终类型必须在所有配置中一致地解析。

我试验过把横屏和竖屏的两个布局中,竖屏的是Button,横屏的是EditText,它们都是TextView的子类,于是把它们都设置tools:viewBindingType="TextView",运行时是竖屏,程序没崩,横屏后程序就崩了,异常如下:

Caused by: java.lang.IllegalArgumentException: Wrong state class, expecting View State but received class com.google.android.material.button.MaterialButton$SavedState instead. This usually happens when two views of different type have the same id in the same hierarchy. This view’s id is id/nameText. Make sure other views do not use the same id.

然后我把竖屏的Button改为CheckedTextViewCheckedTextView也是TextView的子类,运行时竖屏正常,然后转到横屏也正常,然后再转回竖屏就崩了,异常如下:

Caused by: java.lang.ClassCastException: android.widget.TextView.SavedState cannot be cast to android.widget.CheckedTextView.SavedState

实验证明,不同的两个布局,如果类型不同,虽然有共同的父类,但是还是有可能出问题的,经实验,发现这个问题不是ViewBinding带来的,即使我们不使用ViewBinding就使用最原始的方式,也会导致异常,如下:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val nameText: TextView = findViewById(R.id.nameText)
        nameText.text = "你好,中国!"
    }
}

这里我们使用了原始的findViewById,依然会报前面的问题。于是使用官方的例子,BottomNavigationViewNavigationRailView,父类为NavigationBarView,这个就没有问题。

与 findViewById 的区别

与使用 findViewById 相比,视图绑定具有一些很显著的优点:

  • Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null 指针异常的风险。此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用 @Nullable 标记。比如subtitle属性只出现在横屏的布局中,则它是可空类型的,使用如下:

    binding.subtitle?.let {
    	it.text = "I am subtitle."
    }
    
  • 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。

这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。

与数据绑定的对比

视图绑定和数据绑定均会生成可用于直接引用视图的绑定类。但是,视图绑定旨在处理更简单的用例,与数据绑定相比,具有以下优势:

  • 更快的编译速度:视图绑定不需要注解处理,因此编译时间更快。
  • 易于使用:视图绑定不需要特别标记的 XML 布局文件,因此在应用中采用速度更快。在模块中启用视图绑定后,它会自动应用于该模块的所有布局。

反过来,与数据绑定相比,视图绑定也具有以下限制:

  • 视图绑定不支持布局变量或布局表达式,因此不能用于直接在 XML 布局文件中声明动态界面内容。
  • 视图绑定不支持双向数据绑定。

考虑到这些因素,在某些情况下,最好在项目中同时使用视图绑定和数据绑定。您可以在需要高级功能的布局中使用数据绑定,而在不需要高级功能的布局中使用视图绑定。

绑定方法总结

在每个绑定类上,ViewBinding公开了三个公共静态函数来创建绑定对象,以下是何时使用每个函数的快速指南:

  • inflate(layoutInflater) 没有父视图的情况下使用它,比如在Activity onCreate中使用它。
  • inflate(layoutInflater, parent, attachToParent) 有父视图的情况下使用它,比如在FragmentRecyclerView Adapter(或ViewHolder)中使用它.
  • bind(rootView) 当您已经inflate视图并且只想使用ViewBinding来避免findViewById。这对于将ViewBinding适配到您现有的基础架构中以及使用ViewBinding重构代码时很有用。

关于包含的布局

每一个布局文件都会生成对应的绑定对象,即使这个布局包含在另一个布局中,如下:

<!-- activity_awesome.xml -->
<androidx.constraintlayout.widget.ConstraintLayout>
    <include android:id="@+id/includes" layout="@layout/included_buttons"
</androidx.constraintlayout.widget.ConstraintLayout>
  
<!-- included_buttons.xml -->
<androidx.constraintlayout.widget.ConstraintLayout>
    <Button android:id="@+id/include_me" />
</androidx.constraintlayout.widget.ConstraintLayout>

<include>标签必须有一个 id 才能生成绑定属性。这是视图绑定生成属性所必需的(就像普通视图一样)。生成的绑定属性如下:

public final class ActivityAwesomeBinding implements ViewBinding {
  ...

  @NonNull
  public final IncludedButtonsBinding includes;

如上代码,ViewBindingActivityAwesomeBinding中生成IncludedButtonsBinding对象的引用。

同时使用ViewBindingDataBinding

ViewBindingDataBinding这两个库可以同时使用,当两者都启用时,使用<layout>标签的布局将使用DataBinding来生成绑定对象,而没有<layout>标签的布局将使用ViewBinding来生成绑定对象。

比较View bindingKotlin syntheticsButterKnife

View binding提供了更安全、更简洁的视图查找,所以推荐使用。
在这里插入图片描述