这是《通关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显示不同的图片,可以把它当做课后作业,参考答案在这里。