这是《通关Jetpack》系列的第2篇文章

我没什么能耐,不能给你们更好的生活,唯一能做的,是挡在你们前边。——《误杀》

在上一篇文章中,通过实例介绍了Data Binding的概念和用法,本篇文章则从更丰富的细节上介绍Data Binding的种种用法。

##创建Data Binding的两种方式

在代码里,如果要获取到Data Binding对象,有2种方式

方式一:一步到位的DataBindingUtil

onCreate中,可以同时完成设置布局文件+创建Binding对象两个动作。

1
2
3
4
5
6
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User("Test", "User")
}

或者在FragmentListView或者RecyclerView的初始方法中。

1
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

方式二:单独使用LayoutInflater

如果已经调用了setContentView(),则可以在之后单独使用inflate方法来获取Binding对象。

1
val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())

对于FragmentListView或者RecyclerView也同理。

1
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)

表达式用法

可以在布局文件中使用的表达式

可以在布局文件中使用丰富的表达式,列举如下,暂不举例。

  • 数学运算符 + - / * %
  • 字符串拼接 +
  • 逻辑算式 && ||
  • 二元运算符 & | ^
  • 一元运算符 + - ! ~
  • 三目运算符 ?:
  • 移位运算 >> >>> <<
  • 比较运算 == > < >= <=(注意在xml中需要将<转义写为&lt;
  • instanceof
  • 小括号 ()
  • 字面量 字符、字符串、数字以及null
  • 类型转换
  • 方法调用
  • 属性读取
  • 数组读取 []

NULL则替换

NULL则替换表达式(??)很好用,可以避免很多NPE的场景。

1
2
3
4
// NULL则替换
android:text="@{user.displayName ?? user.lastName}"
// 等价于
android:text="@{user.displayName != null ? user.displayName : user.lastName}"

读取属性值

其实对于 a. public属性 b. 带有getter的属性 c. 可观测的属性ObservableField,它们在xml里的读取写法都是相同的,都是对象.属性

1
android:text="@{user.name}"

读取同一布局文件里其它的View

尽管很少用到,但还是介绍一下。

比如我有两个TextView,id无分别是text_view_nametext_view_nickname,在第二个TextView里可以访问第一个TextView的文本,则可以这么写,需要留意的就是会自动将id转为驼峰命名。

1
2
3
4
5
<TextView
...
android:text="@{textViewName.text}"
...
/>

集合的写法

在xml里同样可以使用集合Data,并通过[]来获取集合中特定的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List&lt;String>"/>
<variable name="sparse" type="SparseArray&lt;String>"/>
<variable name="map" type="Map&lt;String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>

android:text="@{list[index]}"

android:text="@{sparse[index]}"

android:text="@{map[key]}"

String字面量

在xml文件中,如果要使用双引号",则可以将外部的双引号替换为单引号'。或者在应当使用双引号的地方使用反引号。

1
2
3
4
<!-- 单引号替代双引号 -->
android:text='@{map["firstName"]}'
<!-- 反引号替代双引号 -->
android:text="@{map[`firstName`]}"

引用静态资源

引用多个静态资源的语法如下。

1
2
3
android:padding="@{larege ? @dimen/largePadding : @dimen/smallPadding}"
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"

事件处理

Data binding允许你为View绑定各种各样的事件处理函数,作为布局文件里的一项属性,大部分命名格式为android:onXXXX,对应的View接口为View.OnXXXXListener。有两种方式处理事件。

  • 方法引用:在表达式中使用方法签名。Data Binding会将方法与对象包装成一个Listener并设置给View。
  • 监听绑定:Lambda表达式,Data Binding对此同样生成一个Listener,供事件触发时调用。

方法引用

就像你可以为onClick指明绑定的方法一样,也可以为View的各种事件绑定ViewModel中的方法。在编译时会对此进行检查,如果方法不存在,或是签名错误,则直接报错。“方法引用”会在编译时创建一个Listener,相应的,“监听绑定”则在事件触发时才创建Listener。一个方法引用绑定的例子如下。

1
2
3
class MyHandlers {
fun onClickFriend(view: View)
}

我们希望在onClick时触发onClickFriend方法,则写法如下。注意:表达式中的签名与类文件里面方法签名必须完全一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.MyHandlers"/>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:onClick="@{handlers::onClickFriend}"/>
</LinearLayout>
</layout>

监听绑定

监听绑定的自由度更大,它允许你运行任意的代码。限制之处则在于,方法的返回值必须与表达式期望的值相匹配。

1
2
3
class Presneter {
fun onSaveClick(task: Task) {}
}

我们可以将以上方法绑定在View的onClick上面。因为这里多了task参数,故无法使用方法引用来写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
</LinearLayout>
</layout>

如果需要用到View本身,则可以使用android:onClick="@{(view) -> presenter.onSaveClick(task)}"。如果处理函数里也要用到View,则函数签名写作。

1
2
3
class Presenter {
fun onSaveClick(view: View, task: Task)
}

还可以为方法增加更多的参数,比如对于CheckBoxisChecked属性。

1
2
3
class Presenter {
fun onCompletedChanged(task: Task, completed: Boolean){}
}
1
2
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

对于返回值非void的情况,函数签名必须匹配。比如onLongClick方法,要返回boolean类型的值,表示事件是否被消费。

1
2
3
class Presenter {
fun onLongClick(view: View, task: Task): Boolean { }
}
1
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"

也可以在表达式里使用三目运算符,用void表示不响应事件。

1
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

忠告:把复杂的业务逻辑放在Kotlin/Java代码中处理,而非xml表达式。

导入、变量与引入

  • 导入(Imports):在xml中引入外部类
  • 变量(Variables):声明在xml中使用的变量
  • 引入(Incluces):帮助我们构建更加复杂的UI

导入

<data>块进行导入,导入后就可以在xml表达式里使用相应的类。如下例的View类。

1
2
3
4
5
6
7
8
9
<data>
<import type="android.view.View"/>
</data>

<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

可以在表达式中进行强制类型转换,如把user.connection强转为User

1
2
3
4
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

在import后,可以在表达式中使用静态方法。

1
2
3
4
5
6
7
8
9
<data>
<import type="com.example.MyStringUtils"/>
<variable name="user" type="com.example.User"/>
</data>

<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

作为Data Binding类库的默认行为,java.lang.*已经自动引入。

变量

声明变量,从而在表达式中引用这些变量,变量的赋值来自于Binding对象。对于同名的portraitlandscape布局文件,它们的变量声明会进行合并,因此注意在处理这种场景时,不要发生命名冲突。对于未赋值的变量,在运行时会取它们的默认值:0falsenull等等。

Data Binding类库同样内置了context变量,供调用者在表达式中使用,它等价于View.getContext()

引入

在进行复杂UI构件时,如果用到了include标签,可以将变量从父布局传递给子布局。如下例,user对象被传递给了子布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</LinearLayout>
</layout>

但是,Data Binding并不支持merge标签,试图在merge过程中传递变量是无效的。

【未完待续】