ViewBinding
breewf

Android绑定View的发展史

从Android系统诞生至今,在代码中findView一直是Android开发者无法绕开的一道程序。从最初的findViewbyId到如今炙手可热的ViewBinding,期间涌现出了许多findView的方式,它们让findView变得更加简单,也让我们的代码变得更加简洁。但随着Android新技术的发展,这些findView的方法也正在被一个一个的抛弃。本节内容我们就来回顾一下Android开发中findView的发展史。

findViewById

findViewById是Android开发中最原始,也是最基础的一种获取View的方法。它由Google官方提供,在Android开发生态的早期也是唯一一种能够获取View的方式。虽然它使用简单且根正苗红,贯穿古今,但由于高度重复的代码结构深受开发者诟病。在一个复杂布局的页面仅仅是findViewById的代码就很多。开发者无时无刻不想着弃用这一方案,因此后续衍生出了多种获取View的方式来简化代码。但万变不离其宗,归根结底,这些方式最终都还是通过findViewById来实现的。

ButterKnife

就在大家都在唾弃findViewById的大量重复代码时,一个插件横空出世。它通过一个BindView注解,传入一个Resource Id就能轻松获取到Id对应的View。代码如下:

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends AppCompatActivity {

@BindView(R.id.text_view) TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}

它就是红极一时,时至今日大家依然还在用着的ButterKnife。ButterKnife通过Java编译时注解处理器,在编译时自动生成findViewById的代码。例如,上边的例子通过ButterKnife会生成一个MainActivity_ViewBinding 类,在这个类中通过findViewById为mTextView赋值,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;

@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}

@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;

target.mTextView = Utils.findRequiredViewAsType(source, R.id.text_view, "field 'mTextView'", TextView.class);
}

@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;

target.mTextView = null;
}
}

这一操作省去了开发者手动编写findViewById的时间,大大简化了代码,同时提高了开发效率。在开发者看来ButterKnife不得不说是一个神器,以至于到后来成了Android项目开发的标配。

但是Android Studio更新到了4.1版本后,发现项目中使用ButterKnife注解id的代码出现了警告,警告信息如下:

Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them as annotation attributes

从警告信息中可以看到在Gradle 5.0的插件中Resource的Id值将不会再是final类型因此应该避免在注解属性中使用Id。这意味着当我们把Gradle插件升级到5.0版本之后ButterKnife将无法再被使用!同时,我们在ButterKnife的官方文档上也看到了ButterKnife被标注弃用的信息:

陪伴我们多年,曾经辉煌一时,不可一世的ButterKnife也要寿终正寝,即将迎来它生命的终点。

DataBinding

DataBinding是Google官方在2015年谷歌I/O大会上发布的一个数据绑定框架,它并非专为findView而生,而是作为MVVM架构的双向绑定数据的工具。findView的功能仅仅是DataBinding的一个附赠品。

开发者一般会在MVVM架构的项目中使用DataBinding来获取View。但是它也有很多诟病,比如需要修改xml的结构,在xml外部嵌套一个标签。并且很多情况下需要手动build才能生成DataBinding相关类。诸如此类问题,自然不会得到开发者的青睐。

关于DataBinding的详细使用在这里不做探讨。

Kotlin Android Extensions

2017年Google I/O开发者大会中,Google宣布Kotlin成为Android开发的一级语言,自此,Kotlin “转正”与Java并驾齐驱。而JetBrain推出的Kotlin Android Extension(以下简称KAE)插件成为了有史以来最简单的获取View的方法,简单到无需任何代码,直接通过id作为View使用。这一功能足以让所有Android开发者抓狂,纷纷感叹这才是findView的未来啊,终于可以和裹挟开发者十多年的findViewById说拜拜了! 作为一个Android开发者,不知道你是否会好奇Kotlin是如何将Id作为View的?我们不妨写一个简单的例子:

1
2
3
4
5
6
7
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView.text = "Test"
}
}

布局文件中TextView的id设置为“textView”,则在Activity中可以直接将textView作为一个TextView来使用。我们通过Android Studio的工具将kotlin的字节码反编译成Java代码看下。

通过上述操作,打开kotlin的字节码后,再通过Decompile反编译成Java代码,则会得到如下图所示的结果:

通过反编译得到的Java代码我们发现Kotlin的这一操作其实也是通过findViewById实现的。只是通过插件的方式让我们感觉上是用了View的Id。

通过Kotlin的扩展插件来find view,无疑是一种优秀的方案。但这一方案并不是无懈可击。它存在以下几个缺点:

  • 类型安全:res下的任何id都可以被访问,有可能因访问了非当前Layout下的id而出错
  • 空安全:这主要体现在配置中的对应布局不全时,运行时可能出现NPE
  • 兼容性:只能在kotlin中使用,java不友好
  • 局限性:不能跨module使用

也正是这几个缺点导致了KAE的大溃败。随着Google对亲儿子ViewBinding的大力推广,KAE最终也招架不住,只能缴械投降—Jetbrains在官网宣布废弃KAE,并推荐开发者使用ViewBinding。

ViewBinding

ViewBinding是Google在2019年I/O大会上公布的一款Android视图绑定工具。它的使用方式有点类似DataBinding,但相比DataBinding,ViewBinding是一个更轻量级、更纯粹的findViewById的替代方案。它具有以下几个优点:

  • 类型安全: ViewBinding会基于布局中的View生成类型正确的属性。比如,在布局中放入了一个 TextView ,视图绑定就会暴露出一个 TextView 类型的属性供开发者使用。
  • 空安全:ViewBinding会检测某个视图是不是只在一些配置下存在,并依据结果生成带有 @Nullable 注解的属性。所以即使在多种配置下定义的布局文件,视图绑定依然能够保证空安全。
  • ViewBinding生成的绑定类是一个Java类,并且添加了Kotlin的注解,可以很好的支持 Java 和 Kotlin 两种编程语言。

同时,Google官方还给出了一个ViewBinding、ButterKnife以及KAE的对比,如下图:

ViewBinding使用详解

开启ViewBinding

Android Studio对于ViewBinding的支持是从3.6版本开始的,AS 3.6版本内置了Gradle插件。只需要在build.gradle中通过以下配置即可开启ViewBinding:

1
2
3
4
5
6
// 需要 Android Gradle Plugin 3.6.0
android {
viewBinding {
enabled = true
}
}

在 Android Studio 4.0 中,viewBinding 变成属性被整合到了 buildFeatures 选项中,所以配置要改成:

1
2
3
4
5
6
// Android Studio 4.0
android {
buildFeatures {
viewBinding = true
}
}

如果,你的项目存在多个模块,则需要在每个模块的gradle中添加上述配置。完成以上配置后ViewBinding会为所有布局文件自动生成对应的绑定类。且无须修改原有布局的 XML 文件,ViewBinding会根据现有的布局自动完成所有工作。

在Activity中使用ViewBinding

首先编写activity_main.xml的布局文件,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<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=".MainActivity">

<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

完成后gradle插件会自动生成一个名为ActivityMainBinding的Java类,在Activity中通过ActivityMainBinding获取Binding实例,如下:

1
2
3
4
5
6
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.textView.text = "Hello World"
}

ViewBinding与include标签

在项目开发中,通常我们会使用include标签来简化布局文件,那么在使用了include标签的布局文件中,应该如何使用ViewBinding呢?且看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// activity_main.xml
<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=".MainActivity">

<include
android:id="@+id/include"
layout="@layout/layout_include" />

</androidx.constraintlayout.widget.ConstraintLayout>


// layout_include.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/tv_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

上述两个布局文件会分别生成ActivityMainBinding与LayoutIncludeBinding两个Java类,并且ActivityMainBinding类中通过组合依赖了LayoutIncludeBinding类。因此,使用方式如下:

1
2
3
4
5
6
7
8
9
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 从ActivityMainBinding中获取LayoutIncludeBinding
val include = binding.include
// 通过LayoutIncludeBinding为TextView赋值
include.tvText.text = "Hello World"
}

如果layout_include.xml文件位于子模块,经实践与以上代码的使用方式并无任何差异,但一定要在子模块中开启ViewBinding才行。

ViewBinding在Fragment中的使用

在Fragment中使用ViewBinding与Activity中有些差异,这里为了简便,我们使用上述中的activity_main.xml作为Fragment的布局文件,则Fragment的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private lateinit var binding: ActivityMainBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = ActivityMainBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textView.text="Hello World"
}

ViewBinding在RecyclerView#Adapter中的使用

布局文件不再贴出,直接看Adapter的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TestAdapter : RecyclerView.Adapter<TestViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestViewHolder {
val binding =
ItemTestBinding.inflate(LayoutInflater.from(parent.context))
return TestViewHolder(binding)
}

override fun onBindViewHolder(holder: TestViewHolder, position: Int) {
holder.binding.textView.text = "Hello World"
}

override fun getItemCount(): Int {
return 10
}

class TestViewHolder(var binding: ItemTestBinding) :
RecyclerView.ViewHolder(binding.root)
}

通过以上几个实例可以看到ViewBinding的使用是非常简单的。而ViewBinding的实现原理也并不难,Gradle插件会根据布局文件在项目的build目录下生成相应的ViewBinding类,并且,最终也是通过findViewById来完成View的获取的。

其他

ButterKnife被弃用,ViewBinding才是findView的未来?

使用视图绑定替代 findViewById