Jetpack ViewBinding

在 Activity 中绑定布局中的控件一般有三种实现方式

  • 使用最原生态的 findViewById

    过于繁琐

  • 使用 ButterKnife 开源框架实现

    ButterKnife 是一个专注于 Android 系统的 View 注入框架,可通过 @BindView 注解声明视图组件,并且可结合插件自动生成对应布局文件的所有资源 id。(但是 ButterKnife 对组件化的支持却很不友好,在 library 中,必须将 R 修改为 R2 才可以正常使用注解功能,这对于组件化项目可以灵活地在 library 和 application 之间切换这一特性来说非常不友好)

  • 使用 Kotlin 扩展插件来获取视图控件

    但在 Kotlin 1.4 版本中废弃了这个扩展插件。这种方式存在一定的弊端:

    • 使用局限性:无法跨模块操作,如业务模块无法使用基础模块中的公共布局。
    • 类型不安全:不同的资源文件可以存在相同的控件 id,因此在 View 层存在引用 id 来源出错的问题。

ViewBinding 提供了视图绑定功能,为开发者提供了更简便的方式编写与视图交互的代码。与 DataBinding 区别在于,它功能着重点在于视图,对 XML 无侵入,效率高,而 DataBinding 在于视图和数据,对 XML 有侵入,效率低。

ViewBinding 相比于 findViewById 方法,ViewBinding 具有如下明显的优点:

  • 具有 Null 安全:由于视图绑定会对视图直接引用,因此不存在因视图 id 无效而引发空指针异常的风险。
  • 具有类型安全:每个绑定类中的字段均具有与他们在 xml 文件中引用的视图相匹配的类型,因此不存在强制转换可能导致的异常问题。

准备工作

在相应模块下添加配置

build.gradle(:app)
1
2
3
4
5
6
7
8
9
10
11
12
13
// Android Studio 3.6
android {
viewBinding {
enabled = true
}
}

// Android Studio 4.0
android {
buildFeatures {
viewBinding = true
}
}

启用 ViewBinding 功能的配置是对整个模块而言的,即会为整个模块的所有布局文件生成对应的绑定类。如果某个布局文件不需要的话,可在相应布局文件中通过 tools:viewBindingIgnore=”true” 属性来设置。

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true">

</androidx.constraintlayout.widget.ConstraintLayout>

这样系统就不会为该 xml 文件自动生成绑定类了。

Activity 中使用

配置完成后,系统会为该模块中的每个 XML 布局文件生成一个绑定类,这个绑定类的命名就是 XML 文件的名称转换为驼峰式,并在末尾添加“Binding”。

以 activity_main.xml 布局为例,系统自动生成的绑定类名称为 ActivityMainBinding。绑定类可以直接引用布局内所有具有 id 的视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MainActivity : AppCompatActivity() {
// 声明 ActivityMainBinding 类型变量
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 通过 inflate 方法获取生成绑定类的实例
binding = ActivityMainBinding.inflate(layoutInflater)
// 设置根视图
setContentView(binding.root)
// 通过绑定类实例操作包含 id 的任意控件
binding.tv.text = ""
}
}

Fragment 中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainFragment : Fragment() {

private var fragmentMainBinding:FragmentMainBinding?=null

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

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

流程基本与 Activity 中一致,但需要注意一点,在 onDestroyView 方法中将绑定类实例赋值为 null。Fragment 的存在时间比其视图时间长,所以需要在 onDestroyView 方法中清除对绑定类实例的所有引用,否则可能存在内存泄漏的风险。

ViewBinding 的封装优化

以在 Activity 中的使用方式为例,ViewBinding 组件的使用流程基本是固定的,主要分为三步:

  • 调用生成的绑定类中的 inflate() 方法来获取绑定类的实例。
  • 通过调用绑定类的 getRoot() 方法获取对应根视图。
  • 将根视图传递到 setContentView() 中,并于当前 Activity 绑定。

在基础业务开发中,经常会定义一个 BaseActivity 处理所有 Activity 的相同业务逻辑,而 ViewBinding 使用流程也是固定的,因此将这部分逻辑封装在 BaseActivity 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class BaseVBActivity<T: ViewBinding> : BaseActivity() {

// 声明一个泛型 T 且父类为 ViewBinding 类型的变量
lateinit var mViewBinding:T

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// mViewBinding 的初始化变量与具体的 xml 布局有关,
// 所以提供 getViewBinding 抽象方法,将其交给子类去实现
mViewBinding = getViewBinding()
// 设置对应根布局
setContentView(mViewBinding.root)
}
abstract fun getViewBinding(): T
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AdvertisingActivity : BaseVBActivity<ActivityAdvertisingBinding>() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

mViewBinding.tvAdvertisingTime.text = ""
mViewBinding.btnIgnore.setOnClickListener{
...
}
}

override fun getViewBinding(): ActivityAdvertisingBinding {
return ActivityAdvertisingBinding.inflate(layoutInflater)
}
}

相应 Activity 继承自 BaseVBActivity 后,只需重写 getViewBinding 方法获取绑定类的实例就可以直接在 Activity 中通过 mViewBinding 直接引用视图控件。

Fragment 中 ViewBinding 的封装方式与 BaseActivity 基本一致。


源码分析

当项目模块的 build.gradle 中开启 ViewBinding 功能之后,若进行项目编译,就会扫描 layout 下所有的布局文件,并生成对应的绑定类。这一点是由 gradle 插件实现的。

在绑定类中会发现,调用了 inflate 之后会调用 bind 方法,而 bind 方法依然是通过 findViewById 绑定的,getRoot 方法返回的即为根布局的 View,如 LinearLayout。

不管采用哪种实现方式,最终都会转化为由 findViewById 函数实现,ButterKnife 框架也是如此。不过与 ViewBinding 不同的是,ButterKnife 是通过 APT 运行时注解生成的 ViewBinding 类实现的,而 ViewBinding 是通过编译时扫描 layout 文件生成的 ViewBinding 类。


其他

遇到的问题

1、报错如下:

1
Cannot access 'androidx.viewbinding.ViewBinding' which is a supertype of 'com.example.testapplication.databinding.ActivityMainBinding'. Check your module classpath for missing or conflicting dependencies

最后也没在网上找到解决办法。只能将 Android Studio 版本由 4.1.2 升级为 4.2.1 解决。


备注

参考资料

kotlin-android-extensions插件也被废弃了?扶我起来

视图绑定

《Android Jetpack开发 原理解析与应用实战》

欢迎关注微信公众号:非也缘也