在Android开发过程中,当我们需要使用到布局中的组件,为其进行赋值或者设置监听等操作时,我们往往需要先获取到其对象,在最初的Android开发中,我们需要使用findViewById方法,通过组件的id来获取其对象。但是,这种最原始的方法,在xml布局较为复杂时,布局中可能存在十多个甚至几十个,此时如果我们仅仅使用findViewById来获取View对象的话,就会多写很多重复代码,而且显得臃肿和不优雅。而作为开发者,我们当然需要尽量避免这种情况的发生,为了减少视图绑定的工作量和重复代码,ButterKnife、
DataBinding、
Android Kotlin Extensions等应运而生。
但是这些方法,或多或少都存在这一些缺陷,而google在AndroidStudio 3.6 版本后推出了ViewBinding,一方面可以让代码更加简洁并且提高编译速度防止空指针。另一方面AndroidStudio是支持ViewBinding进行关联互动的,所以让你在代码与xml之间的跳转更方便。
首先,让我们来学习下ViewBinding的基本使用。对于ViewBinding而言,其使用步骤主要分为三步,如下:
在Android Studio3.6即之后版本,我们可以使用ViewBinding,该功能可按模块启用,如果我们需要在某个模块中启用视图绑定,我们只需要将viewBinding
元素添加到其对应的 build.gradle
文件中,如下所示:
android {
...
viewBinding {
enabled = true
}
}
开启ViewBinding后,系统会自动为每个xml布局生成一个对应的Binding类,如果我们在某个布局中不希望使用到ViewBinding,那么,我们只需要在其根View下指定 tools:viewBindingIgnore="true"
即可,如下:
<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=".viewbinding.ViewBindingActivity"
tools:viewBindingIgnore="true">
androidx.constraintlayout.widget.ConstraintLayout>
如此一来,系统在生成绑定类时就会忽略该布局文件。
实际上,对于开发者而言,使用ViewBinding的最重要的一步就是进行视图绑定了,只有完成视图绑定后,我们就可以使用其绑定类的实例来引用View对象了。
为某个模块启用视图绑定功能后,系统会为该模块中包含的每个 XML 布局文件生成一个绑定类。每个绑定类均包含对根视图以及具有 ID 的所有视图的引用。系统会通过以下方式生成绑定类的名称:将 XML 文件的名称转换为驼峰式大小写,并在末尾添加“Binding”一词。而Binding类中会自动生成xml布局中的View引用,并可以以其id的驼峰形式来获取对应的引用。如:存在一个登录界面其xml布局文件为 activity_login.xml
,其中存在一个id为 bt_login
的button,那么其对应生成的Binding类就为ActivityLoginBinding ,其中存在一个btLogin的Button对象。通过该对象,我们就可以对布局中的 bt_login
进行操作了。
不过Binding类不能直接进行实例化,那么,我们应该如何进行视图绑定,获取Binding类的实例呢?在ViewBinding中主要存在以下几个方法:
在系统生成的Binding类中,除了我们xml布局文件中的View对象引用以外,每个Binding类还自带一个root对象,其对应的时布局文件的根View。我们知道,对于Activity的视图绑定,我们都是在onCreate方法中使用setContentView来完成的。实际上使用ViewBinding也是一样,不过,我们需要在设置ContentView之前获取到Binding类的实例,以便于我们在之后的代码中使用,具体如下:
class ViewBindingActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityViewBindingBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_view_binding)
//视图绑定,方法1
mBinding = ActivityViewBindingBinding.inflate(layoutInflater)
//方法2
// mBinding = ActivityViewBindingBinding.bind(LayoutInflater.from(this).inflate(R.layout.activity_view_binding, null))
//使用root对象设置contentView
setContentView(mBinding.root)
}
}
如此一来,我们就完成了Activity的视图绑定了。我们可以看到,实际上没有什么区别,只是在进行setContentView之前,我们通过 inflate
或者 bind
方法获取到了一个Binding类的实例而已,然后使用其root对象来setContentView。
同样的,在Fragment中使用ViewBinding,我们只需要在onCreateView方法中返回view之前,获取到Binding类的实例即可,如下所示:
private lateinit var mBinding: FragmentTestBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//一般写法
// return inflater.inflate(R.layout.fragment_test, container, false)
//viewBinding写法
mBinding = FragmentTestBinding.inflate(inflater)
return mBinding.root
}
对于组合控件的自定义View,即将xml布局封装成一个View时,我们也可以使用ViewBinding,事实上ViewBinding其实与组合控件的自定义View非常相似,就是将每个布局文件封装成了一个Binding对象,其中持有所有子view的引用。其在自定义View中的使用如下:
class MineItemView(context: Context) : ConstraintLayout(context) {
private var mBinding: ItemMineBinding
init {
val view = LayoutInflater.from(context).inflate(R.layout.item_mine, this)
mBinding = ItemMineBinding.bind(view)
}
}
对于Dialog而言,和Activity一样,我们只需要在onCreate方法中获取Binding类实例即可,如下:
private lateinit var mBinding: DialogMyViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DialogMyViewBinding.inflate(layoutInflater)
//设置dialog布局
// setContentView(R.layout.dialog_my_view)
setContentView(mBinding.root)
}
在使用recycleView时,其Adapter中,我们也可以使用ViewBinding,我们知道在RecycleView中每个item类型,都有个ViewHolder与之对应,而且我们需要重写onCreateViewHolder方法,创建每个item类型对应的ViewHolder。在一般情况下,我们都会传入一个View,此时我们就可以根据该View来创建Binding类了,如下:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHodler {
return MineViewHolder(LayoutInflater.from(context).inflate(R.layout.item_mine,parent,false))
}
....
class MineViewHolder(itemView: View) : BaseViewHodler(itemView) {
//一般写法
// var tvName : TextView = itemView.findViewById(R.id.tvName)
// var ivAvatar : ImageView = itemView.findViewById(R.id.ivAvatar)
// var tvAge : TextView = itemView.findViewById(R.id.tvAge)
// var btTest : Button = itemView.findViewById(R.id.btTest)
var mBinding = ItemMineBinding.bind(itemView)
}
然后,我们就可以使用ViewHolder的mBinding来获取对应的view组件了。或者,我们其实也可以这么写,如下:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHodler {
val binding = ItemMineBinding.inflate(LayoutInflater.from(context), parent, false)
return MineViewHolder(binding)
}
...
class MineViewHolder(binding: ItemMineBinding) : BaseViewHodler(binding.root){
var tvName : TextView = binding.tvName
var ivAvatar : ImageView = binding.ivAvatar
var tvAge : TextView = binding.tvAge
var btTest : Button = binding.btTest
}
我们可以看到,实际上无论是在Activity、Fragment还是Dialog、自定义View或者Adapter中使用ViewBinding,其视图绑定方法实际上与之前没有变化,所不同的就是需要获取一个Binding实例供之后使用,之后的操作实际上本质都是一样的,只不过view对象变成了Binding实例的root对象。
在之前的介绍中,我们提到过,在完成视图绑定后,Binding类中会自动生成xml布局中的View引用,并可以以其id的驼峰形式来获取对应的引用,比如我们有如下xml布局文件 activity_view_binding
:
<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=".viewbinding.ViewBindingActivity">
<TextView
android:id="@+id/tv_binding"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="textview"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.808" />
<Button
android:id="@+id/bt_binding"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button"
android:layout_marginBottom="100dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_binding"/>
androidx.constraintlayout.widget.ConstraintLayout>
此时,系统会生成ActivityViewBindingBinding,该类中存在一个TextView——tvBinding和一个Button——btBinding,我们可以通过这两个对象来操作布局文件中的tv_binding和bt_binding,如下:
class ViewBindingActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityViewBindingBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_view_binding)
//视图绑定
mBinding = ActivityViewBindingBinding.inflate(layoutInflater)
setContentView(mBinding.root)
//获取view对象
mBinding.tvBinding.text = "binding success"
mBinding.btBinding.setOnClickListener {
mBinding.tvBinding.text = "click button"
}
}
}
运行结果正常。
ViewBinding的使用非常简单,相对而言需要值得注意的就是布局中存在include和merge的情况。对于include
,当我们想要获取到include中的View时,我们只需要在include对应的对象下寻找即可,如存在以下布局include_test.xml
:
<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
android:id="@+id/tv_include"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="include"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
androidx.constraintlayout.widget.ConstraintLayout>
在Activity的布局中,我们使用include
标签引入该布局,如下:
<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=".viewbinding.ViewBindingActivity">
<include
android:id="@+id/include_test"
layout="@layout/include_test"/>
androidx.constraintlayout.widget.ConstraintLayout>
此时,我们想要引用include布局中的textView,我们可以使用mBinding.includeTest.tvInclude
,如下:
mBinding.includeTest.tvInclude.text = "viewBinding include"
注意:此时include
标签的根View不能有id
但是,当我们将include_test.xml
的根视图改为merge
时,这样是不可行的,此时运行会报错。那么这时,我们应该怎么办呢?因为每个xml布局文件都存在对应的Binding类,此时,我们只需要将include
标签中的布局文件的Binding类绑定到根View下即可,如下:
val includeTestBinding = IncludeTestBinding.bind(mBinding.root)
includeTestBinding.tvInclude.text = "this is a test message"
注意:此时include
标签不能有id,否则会报错
目前,谷歌在AndroidStudio4.1及以上版本新创建的项目已默认移除kotlin-android-extensions插件,官方更推荐使用ViewBinding、findViewById和DataBinding。那么这些视图绑定的方法都有些什么区别和优势呢?
Null 指针异常
的风险和类型转换
的风险;在布局复杂时使用不便。layout
标签进行标注,更加方便。当然DataBinding支持数据双向绑定和布局表达式,这是ViewBinding没有的,二者可以相互补充。如下图所示:
ViewBinding的使用方法非常简单,实际上ViewBinding就可以看作是将每个xml布局文件都封装成了一个自定义的View,只不过这个自定义View只保存了其中的子控件的对象引用,而没有其它的功能。在之前的项目开发中,一直都是使用的kotlin-android-extensions,虽然这确实十分方便,但是不得不说,这种方法还是存在着一些bug,容易在项目中出现一些未知的问题,而且也已经被谷歌抛弃。相对而言ViewBinding虽然多了个视图绑定的操作,但是其更加稳定,还是值得的,以后在项目中尽量去使用ViewBinding吧。