当你停止奋斗的时候,你就真的老了。
本文翻译自 Android Data Binding codelab(干咱们这行需要科xiao上网,累还得爱),主要讲解了 Android Data Binding 库的基本使用方法。本 codelab 所讲项目的 GitHub 地址。
目录
8. 使用 Binding Adapters(绑定适配器)创建自定义属性
1. 介绍
Data Binding(数据绑定) 库
Data Binding 库允许您使用声明的格式而不是编程方式将布局中的 UI 组件绑定到应用程序中的数据源。在本 codelab 中,你将学习如何设置所有内容、使用 布局表达式、使用 可观察对象 以及创建自定义的 Binding Adapters(绑定适配器),以使模板最少化。
你将构建什么
在本 codelab 中,你将把这个 app 转换成(使用)Data Binding(的 app):
这个 app 只有一个展示一些静态数据和可观察数据的屏幕页面,意味着当数据更改时,UI 将 自动更新。
数据是由 ViewModel 提供的。Model-View-ViewModel 是一种展现层模式,它非常适合于数据绑定。下面是一个图表
如果你还不熟悉 Architecture Components 库中的 ViewModel 类,可以查看官方文档。总之,它是一个为视图(Activity、Fragment等)提供 UI 状态的类。(屏幕)方向改变时它依旧存在,并且充当了与应用程序中其他层的接口。
你需要什么
- Android Studio 3.2 或更高版本。
2. 试试不使用 Data Binding 的 app
在这一步中,你下载完整的 codelab 代码,然后运行一个简单的示例 app。
$ git clone https://github.com/googlecodelabs/android-databinding
或者,您可以将存储库下载为zip文件:DOWNLOAD ZIP
- 解压代码
- 在 3.2 或更新版本的 Android Studio中打开项目。
运行 app
默认的 activity 打开后像这样:
这个屏幕展示了一些数据,让用户点击按钮来增加一个计数并更新进度条。它使用 SimpleViewModel。打开它看一下。
使用 Ctrl+N 来快速定位 Android Studio 中的类。使用 Ctrl+Shift+N 通过文件名 定位文件。
在 Mac 上,使用 Command+O 来定位类,使用 Command+Shift+O 来定位文件。
它展示了
- 名字(name)和 姓氏(last name)
- 一个(代表) likes 的数字
- 人气
同时,它让用户通过 onLike() 来增加 likes(“喜欢”)的数字。
虽然 SimpleViewModel
没有最有趣的功能,但它仍然有用。另一方面,PlainOldActivity
有许多问题:
- 它多次调用 findViewById。这不仅慢,而且也不安全,因为它在编译时没有检查。如果你传给 findViewById 的 ID 是错的,那么 app 在运行时将会崩溃。
- 它在 onCreate 中设置初始值。如果是自动设置好的默认值那会好得多。
- 它在布局中使用 android:onClick 属性,这也是不安全的:假如 onLike 方法在你的 activity 中没有被实现(或者被重命名),那么 app 在运行时将会崩溃。
- 它有大量代码。Activities 和 fragments 往往会快速增长,所以我们想从它们中移走尽可能多的代码。同时,activities 和 fragments 中的代码很难测试和维护。
使用 Data Binding 库,我们将解决所有这些问题,将逻辑从 activity 移出到可重用和易于测试的地方。
3. 启用 Data Binding 并转换布局
这个项目已经启用了 Data Binding,但是当你想在自己的项目中使用它时,第一步是在 将使用它的 modules 中 启用库。
build.gradle
android {
...
dataBinding {
enabled true
}
}
现在你将把 layout 转换成 Data Binding layout。
要把一个常规 layout 转换成 Data Binding layout,你需要:
- 使用
<layout> 标签包裹你的
layout- 添加 layout 变量(可选)
- 添加 layout 表达式(可选)
打开 plain_activity.xml
。
它是一个将 Constraint Layout(约束布局) 作为根元素的常规 layout。
为了将 layout 转换成 Data Binding 的,你需要在 < layout > 标签中包裹根元素。你也需要把命名空间定义(以 xmlns: 开头的属性)移到新的根元素中。
方便的是,Android Studio 提供了自动完成这一操作的方法。
你的 layout 现在应该看起来像这样:
<layout 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">
<data>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
...
找出 <data> 标签,这就是我们放 布局变量 的地方。
布局变量用于写 布局表达式。布局表达式放在元素属性的值中,它们使用 @{
expression
}
格式。下面是一些例子:
// 一些复杂的布局表达式的例子
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
布局表达式语言非常强大,但是您应该只将其用于基本连接,因为更复杂的表达式将使读取和维护更加困难。
// 将 viewmodel 的 name 属性 绑定到 text 标签上。
android:text="@{viewmodel.name}"
// 将 viewmodel 的 nameVisible 属性 绑定到 visibility 标签上。
android:visibility="@{viewmodel.nameVisible}"
// 当 View 被点击时调用 viewmodel 的 onLike() 方法。
android:onClick="@{() -> viewmodel.onLike()}"
在此查看该语言的完整描述。
让我们绑定一些数据!
4. 创建你的第一个布局表达式
现在让我们从一些静态数据绑定开始。
- 在 < data > 标签里创建两个 String 类型的 布局变量。
<data>
<variable name="name" type="String"/>
<variable name="lastName" type="String"/>
</data>
- 查找 ID 为 plain_name 的 TextView,并用 布局表达式 添加 android:text 属性:
<TextView
android:id="@+id/plain_name"
android:text="@{name}"
... />
布局表达式以 @ 符号开始,并包裹在花括号 { } 中。
因为 name 是一个 String,所以 Data Binding 将知道如何在 TextView 中设置该值。稍后您将学习如何处理不同的布局表达式类型和属性。
- 对 plain_lastName text view 做同样的事(操作)。
<TextView
android:id="@+id/plain_lastname"
android:text="@{lastName}"
... />
你可以在 plain_activity_solution_2.xml
中找到这些操作的结果。
现在我们需要修改 Activity,以便它正确填充 Data Binding 布局。
5. 更改填充,移除 Activity 中的 UI 调用
布局已经准备好了,但是我们需要在 activity 中做一些改变。打开 PlainOldActivity
。
因为我们使用的是 Data Binding 布局,所以填充是以不同的方式完成的。
在 onCreate 中,将:
setContentView(R.layout.plain_activity)
替换为:
val binding : PlainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.plain_activity)
为什么我们要创建一个变量?因为我们需要一种方法来设置我们在 <data> 块中声明的那些布局变量。这就是绑定对象的目的。绑定类是由库自动生成的。如果你想要的话,打开 PlainActivitySolutionBinding
,然后四处看看。它是简单的 Java 代码。
- 现在我们只需要设置变量值:
binding.name = "Your name"
binding.lastName = "Your last name"
就是这样。您只需使用库绑定数据即可。
我们能开始移除旧代码了:
- 移除 updateName( ) 方法,Data Binding 已经找到了 ID 并 设置了文本值。
- 在 onCreate( ) 移除 updateName( ) 调用。
你可以在 PlainOldActivitySolution2 中找到这些操作的结果。
现在你能运行 app 了。你会发现你的名字取代了 Ada 的。
6. 处理用户事件
到目前为止,我们已经探索了如何向用户展示数据,但是使用 Data Binding 库,您还可以处理用户事件和调用布局变量上的操作。
在我们做这些事之前,我们会稍微清理一下我们的布局。
- 首先,将两个变量替换为单个 ViewModel。大多数情况下都这样做,为的是你的表示代码和状态包含在一个地方。
<data>
<variable
name="viewmodel"
type="com.example.android.databinding.basicsample.data.SimpleViewModel"/>
</data>
我们将调用 viewmodel 属性,而不是直接访问变量。
- Change the layout expressions in both TextViews在两个 TextView 中更改布局表达式
<TextView
android:id="@+id/plain_name"
android:text="@{viewmodel.name}"
... />
<TextView
android:id="@+id/plain_lastname"
android:text="@{viewmodel.lastName}"
... />
此外,我们会对点击 "Like" 按钮作出反应。
- 寻找 like_button Button 并将
android:onClick="onLike"
替换为
android:onClick="@{() -> viewmodel.onLike()}"
之前的 onClick 属性使用了一种不安全的机制,在该机制中,当 view 被点击时 activtiy 和 fragment 中的 onLike( ) 方法被调用。如果具有这个精确签名的方法不存在,app 就会崩溃。
新方法更安全,因为它在编译时会被检查,并使用 lambda 表达式来调用 view model 的 onLike( ) 方法。
单击 Android Studio中 的 Build 菜单中的 "Make Project",检查 Data Binding 错误。问题将阻止 app 构建,错误将显示在构建日志中。
你能在 plain_activity_solution_3.xml
中找到这些操作的结果。
现在让我们从 activity 中移除我们不需要的东西:
1. 将
binding.name = "Your name"
binding.lastName = "Your last name"
替换为
binding.viewmodel = viewModel
2. 移除 activity 中的 onLike 方法,因为它现在以已经被绕开了。
你能在 PlainOldActivitySolution3
中找到这些操作的结果。
如果你运行这个 app,你会发现按钮没有任何作用。这是因为我们不再调用 updateLikes( ) 了。让我们恰当地实现它。
7. 观察数据
我们在前面的步骤中创建了一个静态绑定。如果打开视图模型,你会发现 name 和 lastName 只是字符串,这很好,因为它们不会改变。但是,likes 是被用户所修改的。
var likes = 0
当这个值改变时,我们不需要显式地更新 UI,而是要使它变得可观察。
使用数据绑定,当可观察值改变时,它绑定到的UI元素将自动更新。
有多种实现可观察性的方法。您可以使用可观察的类、可观察的变量,或者首选的方式,LiveData。关于这方面的完整文件是在这里。
我们将使用 ObservableFields ,因为他们比较简单。
将
var likes = 0 // 在这个 codeLab 中这是一个严格的强类型,因此我建议不要使用 “非常” private 的设置,这样式为了防止外部修改变量。
替换为新的可观察变量:
val likes = ObservableInt()
val popularity = ObservableField<Popularity>(NORMAL)
同时,将
fun onLike() {
likes++
}
/**
* 按量返回受欢迎度: [Popularity.NORMAL],
* [Popularity.POPULAR] 或者 [Popularity.STAR]
*/
val popularity: Popularity
get() {
return when {
likes > 9 -> Popularity.STAR
likes > 4 -> Popularity.POPULAR
else -> Popularity.NORMAL
}
}
替换为
fun onLike() {
likes.set(likes.get() + 1)
popularity.set(likes.get().let {
when {
it > 9 -> Popularity.STAR
it > 4 -> Popularity.POPULAR
else -> Popularity.NORMAL
}
})
}
.如你所见, 可观察变量的值必须用 set() 设置,并用 get() 读取。此机制允许库在值更改时更新 UI。
如果你重新构建项目,你将发现该 activity 没有编译。我们直接从 activity 中获取 likes,我们不再需要这样做:
private fun updateLikes() {
findViewById<TextView>(R.id.likes).text = viewModel.likes.toString()
findViewById<ProgressBar>(R.id.progressBar).progress =
(viewModel.likes * 100 / 5).coerceAtMost(100)
...
打开 PlainOldActivity 删除 activity 中的所有私有方法及其调用。现在的 activity 变得很简洁。
class PlainOldActivity : AppCompatActivity() {
// 从 ViewModelProviders 中获取 ViewModel
private val viewModel by lazy { ViewModelProviders.of(this).get(SimpleViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding : PlainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.plain_activity)
binding.viewmodel = viewModel
}
}
你能从 SolutionActivity
中找到这些操作的结果。
一般来说,将代码从 activity 中移出对于可维护性和可测试性而言是非常重要的。
让我们将显示 likes 次数的TextView 绑定到可观察的整数。在 plain_activity.xml 中:
<TextView
android:id="@+id/likes"
android:text="@{Integer.toString(viewmodel.likes)}"
...
如果你现在运行 app,likes 的数量会按预期增加。
到目前为止,我们已经使用了像 android:onClick 和 android:text 这样的属性。让我们更探索其它属性并创造我们自己的(属性):
8. 使用 Binding Adapters(绑定适配器)创建自定义属性
当你将一个字符串(或者一个可观察的字符串)绑定到 android:text 属性时,会发生什么是很明显的,但是它是如何发生的呢?
使用 Data Binding 库,几乎所有 UI 调用都是在被称为 Binding Adapters 的静态方法中完成的。
该库提供了大量的 Binding Adapters。在这里查看它们。下面是一个关于 android:test 属性的例子:
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
// 为了清晰,删除了一些检查。
view.setText(text);
}
或者 android:background 的一个例子:
@BindingAdapter("android:background")
public static void setBackground(View view, Drawable drawable) {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
view.setBackground(drawable);
} else {
view.setBackgroundDrawable(drawable);
}
}
Data Binding 并没有什么神奇之处。所有问题都在编译时被解决,你可以在生成的代码中阅读。
让我们来研究进度条。我们希望它:
- 若果没有 likes,就隐藏
- (被)5个 likes 填满
- 填满后改变颜色
我们将为此创建自定义 Binding Adapters:
在 utils 包中打开 BindingAdapters.kt
文件。不管你在哪里创建它们,库都会找到它们。在 Kotlin 中,可以通过向 Kotlin 文件的顶层添加函数来创建静态方法。
为第一条要求寻找 Binding Adapter,即 hideIfZero:
@BindingAdapter("app:hideIfZero")
@JvmStatic fun hideIfZero(view: View, number: Int) {
view.visibility = if (number == 0) View.GONE else View.VISIBLE
}
这个绑定适配器:
- 应用于 app:hideIfZero 属性。
- 可以应用于每个 View(因为第一个参数是 View;你可以通过更改此类型来限制某些类)。
- 取一个应该是布局表达式返回的整数。
- 如果数字是零的话就让 View GONE。
在 plain_activity 布局中,寻找进度条并添加 hideIfZero 属性:
<ProgressBar
android:id="@+id/progressBar"
app:hideIfZero="@{viewmodel.likes}"
...
运行 app,当你第一次点击按钮时,你将看到进度条。然而,我们仍然需要改变它的值和颜色:
对于进度值,我们将使用Binding Adapter,它获取 likes 数量的最大值。打开BindingAdapters
文件并查找这个(内容):
/**
* 设置进度条的值以使5个 likes 填满它。
*
* 具有多个属性的 Binding Adapters 示例。
* 请注意,每当任何属性更改时,都会调用该适配器。
*/
@BindingAdapter("app:progressScaled", "android:max", requireAll = true)
@JvmStatic fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) {
progressBar.progress = (likes * max / 5).coerceAtMost(max)
}
如果缺少任何属性,则不使用此 Binding Adapter。这在编译时发生。该方法现在接收3个参数(它所应用于的视图 + 在 annotation 中定义的属性的数量)。
当绑定适配器被使用时,requireAll 参数定义了:
- 当为 true 时,所有元素必须在 XML 定义中展现。
- 当为 false 时,缺失的属性将为null,如果为boolean类型,则为false,如果为基本类型,则为0。
接下来,将属性添加到 XML:
<ProgressBar
android:id="@+id/progressBar"
app:hideIfZero="@{viewmodel.likes}"
app:progressScaled="@{viewmodel.likes}"
android:max="@{100}"
...
我们将 progressScaled 属性绑定到 likes 的数量,并且只向 max 属性传递一个文本整数。如果不添加 @ {} 格式,Data Binding 将无法找到正确的 Binding Adapter。
你可以在 plain_activity_solution_5.xml
中找到这些步骤的结果。
如果运行应用程序,你将看到进度条如何按预期填充。
10. 练习创建 Binding Adapters
孰能生巧。创建:
- 一个根据 likes 的值对进度条的颜色进行着色,并添加相应的属性的 Binding Adapter。
- 一个根据受欢迎度来显示不同的图标的 Binding Adapter:
黑色的 ic_person_black_96dp
浅粉红色的 ic_whatshot_black_96dp
深粉红色的
ic_whatshot_black_96dp
您将在 BindingAdapters.kt 文件、SolutionActivity 和 solution.xml 布局中找到解决方案。
11. 你一定会成功!
祝贺你!你完成了 codeLab,因此你应该知道如何创建 Data Binding 布局、添加变量和表达式,使用可观察数据,并通过自定义 Binding Adapter 来自定义属性,让你的 XML 布局更有意义。