这是《通关Jetpack》系列的第1篇文章
有些人沦为平庸,有的人金玉其外、败絮其中。可未来某一天,不经意间你会遇到一个彩虹般绚丽的人,从此以后,其他人就不过是匆匆浮云。
Data Binding是什么
在学习一项新知识、新技能之前,必须问的问题是这门技术是什么、将为我们带来什么有益的改变。我们来看一下Data Binding。
Data Binding是什么
Data Binding是由Google推出的一个库(Library),用来解决数据变化与UI显示同步的问题。
Data Binding常常与MVVM一同出现,这并不意味着它们是同一类东西。MVVM是设计模式/编程思想,ViewModel是其中一个组成部分。而Data Binding则是实现这门思想的一种工具。
Data Binding将为我们带来怎样的改变
做应用开发,很大一部分精力都用于处理UI和数据的同步问题。在传统实现方式里,数据从服务器返回了,此时要手动调用updateView
一类的方法更新UI;同样,当用户操作UI上的元素时,会通过Listener
等监听,更新到内存的数据中——这是逻辑式写法,而Data Binding则是通过声明式的写法,将数据-UI解耦,从而可以简化控制流,让我们把注意力集中在复杂的业务逻辑上,而非UI同步。
我们首先从codelab的实例入手,学习如何使用Data Binding。然后会深入介绍Data Binding的API,最后则是从原理角度阐述它是如何工作的。预计分成3篇文章。
假想一种需求场景
以这样一个App为例,它需要显示一段静态的文本(左侧),以及可点击的LIKE按钮(右侧),点击该按钮时会提升欢迎度数值并实时显示在UI上,最后,欢迎度数值每达到一个阶段,会在屏幕右侧显示不同阶段的图片。
没有Data Binding时的写法
首先是没有使用Data Binding时,传统的MVVM写法,已省略无关代码。
1 | // SimpleViewModel.kt |
这么简单一个页面,光是列出来的缺点就有6条之多,无法想象当业务逻辑复杂起来后,代码将会增长到何种程度。接下来,让我们看看Data Binding如何解决上述问题的。
静态数据绑定UI
gradle从1.5.0版本开始支持data binding首先需要在project的build.gradle
文件里打开开关,
1 | android { |
相比于普通布局,data binding布局文件在最外层增加了<layout>
标签,<layout>
标签内部则由<data>
标签和原布局组成。对于需要重构成data binding的布局文件,将鼠标选中最外层布局,会自动提示 Convert to data binding layout。一个data binding布局如下所示:
1 | <layout xmlns:android="http://schemas.android.com/apk/res/android" |
<data>
标签中的变量,可以在布局文件里进行调用,也支持简单的表达式,比如if...else...
、类型转换、字符串拼接。表达式的格式为@{...}
。虽然表达式很强大,但是不要滥用,否则会使布局文件过于复杂,难以维护。
1 | android:text="@{String.valueOf(index + 1)}" |
基于上述知识,我们为属性name和last name声明变量。
1 | <data> |
从而可以在布局文件中直接使用它们。
1 | <TextView |
目前为止已经完成了布局文件的编写,需要一个时机,将数据塞给布局文件,可以在onCreate
里做这件事。
1 | // onCreate |
如此即可,不需要findViewById
、setText
,甚至不关心UI里到底是TextView还是EditText甚至自定义控件,要做的事情只是给成员变量赋值。处理完了静态文本,接下来看看如何响应UI的点击LIKE事件。要知道,这里我们同样也不用findViewById
以及setOnClickListener
的。
首先回忆一下最早的点击事件处理方式——在布局文件中,onClick=onLike
,这样自动关联起来Activity中的onLike
方法,虽然直观,但带来的后果就是绑死了布局文件与Activity,根本无法复用。其实Data Binding的处理方式与此类似,只不过它增加了一个ViewModel层。
在计算机软件领域,没有什么问题是增加一个中间层不能解决的,如果就,那就加两层。
1 | <data> |
<data>
中不再是简单的String,而是一个SimpleViewModel类型对象,该类在上文中可以看到,它包含name
、lastName
字段,以及一个处理LIKE++事件的onLike
方法。有了这个viewmodel
对象,就可以在接下来的布局里面这样使用了。且比上种写法更好的地方在于,编译器会检查onLike
方法是否存在,如果不存在则编译时就会异常。
1 | <TextView |
对于viewmodel
的赋值,则在onCreate里面直接binding.viewmodel = viewModel
就可以。但是如果我们把likes
属性绑定到布局文件的某个TextView上(注意这里不要直接让android:text="@{viewmodel.lines}"
,因为这会导致运行时读取id=0的字符串资源,从而异常),会发现即使通过onLike()
增长了LIKE,但在UI上并没有体现,LIKE数始终显示为0。这是因为目前为止我们进行的绑定都是静态且单向绑定,下一节我们将学习双向绑定,从而让LIKES的数目实时显示在UI上。
双向Data Binding的写法
所谓“双向”Data Binding,是指对于View而言,在初始化时可以自动从data里获取数据,对于data而言,当它们的值发生变化时,能自动通知UI显示相应数值。对于这种发生变化时能通知UI的数据,称之为具有“可观察性”,即observable。有多重途径生成一个可观察的对象,比如observable classes
、observable fields
,以及最通用也最好用的LiveData
。声明可观察对象的方式如下。带有前置下划线_
的变量为私有变量,是可变的,不对外进行暴露。每个私有变量都拥有一个LiveData<T>
类型的接口变量,用于提供给布局文件读取。
1 | private val _name = MutableLiveData("Ada") |
同时需要在Activity的onCreate()
方法中,为binding
对象的lifecycleOwner
赋值(为Activity)。如果不做这一步,数据的变化就无法被观测到。
1 | override fun onCreate(SavedInstanceState: Bundle?) { |
对于popularity
,同样将其声明为LiveData
类型。这里注意,并没有私有的_popularity
,因为它的值实际上是根据likes
计算得到的。因此通过Transformations.map()
操作,从一个MutableLiveData
类型的_likes
对象计算出LiveData
类型的popularity
。关于Transformation
的使用,可以看这篇文章。
1 | // popularity is exposed as LiveData using a Transformation instead of a @Bindable property. |
此时,在onLike()
中我们直接修改_likes
的值,就会 通知到UI自动发生调整了。
1 | fun onLike() { |
此处简单介绍下其实现原理,Data Binding库里面包含众多Adapters(适配器),见源码。对于xml布局文件里的UI组件,需要设置的属性均声明了相应的静态设值方法,比如对于TextView
的android:text
属性,就有如下setText(TextView, CharSequence)
方法。当text值发生变更时,就会调用到这个方法。
1 | "android:text") ( |
此时还剩下两点功能没有完成,分别是LIKE按钮下方的进度条没有实时增加,以及LIKE图片没有更新。先看进度条相关的事项。对于进度条有3点需求,分别是:
- 当LIKES=0时,进度条隐藏
- 当LIKES增加时,进度条实时增长,且每5个LIKE一个循环,进度条归零
- 当进度条拉满时,颜色变深
这些需求都可以通过自定义BindingAdapter
来实现,它可以为xml元素创建任意的自定义属性,并通过代码读取该属性,进而对xml元素原生的属性进行修改。在任意包下创建一个BindingAdapters.kt
文件(不要担心路径,因为编译时会自动识别注解),借助于Kotlin的顶层函数,可以不声明BindingAdapter
类而直接写函数。我们为所有的View
创建一个app:hideIfZero
属性,控制当该属性为0时隐藏UI元素。
当LIKES=0时,进度条隐藏
1 |
|
随后在xml布局文件中将该属性与viewmodel.likes
进行绑定,这样当likes=0时,进度条就会自动隐藏。
1 | <ProgressBar |
当LIKES增加时,进度条实时增长,且每5个LIKE一个循环,进度条归零
参考上面的hideIfZero
属性,我们这次同样用自定义BindingAdapter
来实现。不同之处在于,这次我们需要同时读取两个属性,在@BindingAdapter
注解里面,通过字符串数组["app:progressScaled", "android:max"]
来声明,同时requireAll = true
表示必须两个属性都在xml得到声明时,才会调用该方法进行处理。如果有任一个没有赋值,则不进行方法。与之相对应的requiredAll = false
则对至少一个属性进行相应,对于没有声明的属性会使用其默认值。
coerceAtMost是Kotlin提供的扩展函数,表示“最大不超过max
”。
1 |
|
相应地,在布局文件中声明app:progressScaled
和android:max
两个属性。
1 | <ProgressBar |
当进度条拉满时,颜色变深
如果前面的知识都已经掌握,这里也就不难了。我们声明一个自定义属性app:progressTint
,表示会影响到progressBar的外显颜色,输入为popularity,方法内部先根据popularity计算出color(注意这里使用到的Context为View的Context),然后再将其设置到progressBar的progressTintList
属性上。
1 |
|
随后还有一个功能,是根据不同的popularity显示不同的图片,可以把它当做课后作业,参考答案在这里。