简介
官方链接:
在大多数情况下,ViewBinding
可以替代 findViewById
。每当需要inflate
布局时,都可以使用绑定类,例如Fragment
、Activity
,甚至是 RecyclerView Adapter
(或ViewHolder
)。
启用ViewBinding
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
。
在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
的存在时间比其视图长。请务必在 Fragment
的 onDestroyView()
方法中清除对绑定类实例的所有引用。
绑定已经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
,否则运行时会崩溃。
我们直接在布局中嵌入了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
}
}
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" />
在这种情况下,您可能希望生成类暴露一个类型为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" />
请注意,视图绑定在生成代码时无法验证此属性的值。为避免编译时和运行时错误,该值必须满足以下条件:
-
该值必须是继承自
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
改为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 的区别
与使用 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)
有父视图的情况下使用它,比如在Fragment
或RecyclerView 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;
如上代码,ViewBinding
在ActivityAwesomeBinding
中生成IncludedButtonsBinding
对象的引用。
同时使用ViewBinding
和DataBinding
ViewBinding
和DataBinding
这两个库可以同时使用,当两者都启用时,使用<layout>
标签的布局将使用DataBinding
来生成绑定对象,而没有<layout>
标签的布局将使用ViewBinding
来生成绑定对象。
比较View binding
、Kotlin synthetics
、ButterKnife
View binding
提供了更安全、更简洁的视图查找,所以推荐使用。