官方链接:
在大多数情况下,ViewBinding可以替代findViewById。每当需要inflate布局时,都可以使用绑定类,例如Fragment、Activity,甚至是RecyclerView Adapter(或ViewHolder)。
ViewBinding在Android 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,此类具有两个属性:一个是名为name的TextView,另一个是名为button的Button。该布局中的ImageView没有ID,因此绑定类中不存在对它的引用。
每个绑定类还包含一个getRoot()方法,返回布局的根视图,在此示例中,ResultProfileBinding类中的getRoot()方法会返回LinearLayout。
- 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)。
- 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的存在时间比其视图长。请务必在Fragment的onDestroyView()方法中清除对绑定类实例的所有引用。
对于已经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,否则运行时会崩溃。
我们直接在布局中嵌入了Fragment,Fragment的代码也很简单,如下:
- 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
- }
-
- }
-
使用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" />
-
在这种情况下,您可能希望生成类暴露一个类型为TextView的userBio字段,因为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:viewBindingType为TextView,另一个布局的不用设置,因为另一个布局中的类型已经是TextView了。假设两个布局中的类不是父子关系,但是他们有共同的父类,如果使用父类型即可,则需要在两个布局中都设置tools:viewBindingType,比如你有两个布局,一个包含BottomNavigationView,另一个包含NavigationRailView,这两个类都继承自NavigationBarView,NavigationBarView包含了大部分实现细节。如果你的代码不需要知道关于子类的细节,则可以在两种布局中使用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" />
-
请注意,视图绑定在生成代码时无法验证此属性的值。为避免编译时和运行时错误,该值必须满足以下条件:
- <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改为CheckedTextView,CheckedTextView也是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,依然会报前面的问题。于是使用官方的例子,BottomNavigationView、NavigationRailView,父类为NavigationBarView,这个就没有问题。
与使用findViewById相比,视图绑定具有一些很显著的优点:
- binding.subtitle?.let {
- it.text = "I am subtitle."
- }
-
这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。
视图绑定和数据绑定均会生成可用于直接引用视图的绑定类。但是,视图绑定旨在处理更简单的用例,与数据绑定相比,具有以下优势:
反过来,与数据绑定相比,视图绑定也具有以下限制:
考虑到这些因素,在某些情况下,最好在项目中同时使用视图绑定和数据绑定。您可以在需要高级功能的布局中使用数据绑定,而在不需要高级功能的布局中使用视图绑定。
在每个绑定类上,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;
-
如上代码,ViewBinding在ActivityAwesomeBinding中生成IncludedButtonsBinding对象的引用。
ViewBinding和DataBinding这两个库可以同时使用,当两者都启用时,使用<layout>标签的布局将使用DataBinding来生成绑定对象,而没有<layout>标签的布局将使用ViewBinding来生成绑定对象。
View binding提供了更安全、更简洁的视图查找,所以推荐使用。