官方链接:
在大多数情况下,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提供了更安全、更简洁的视图查找,所以推荐使用。