上一篇介绍了运用 Kotlin DSL 构建布局的方法,相较于 XML,可读性和性能都有显著提高。若是这套 DSL 还能数据绑定就更好了,这样就能去掉 findViewById 和 Activity 中的业务逻辑代码(findViewById 须要遍历 View 树,这是耗时的,而 Activity 中的业务逻辑让其变得愈加臃肿)。这一篇就介绍一种实现思路。android
这是该系列的第十篇,系列文章目录以下:git
在没有 Data Binding 以前,咱们是这样为控件绑定数据的:
class MainActivity : AppCompatActivity() {
private var tvName: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//'获取控件引用'
tvName = findViewById<TextView>(R.id.tvName)
}
override fun onUserReturn(user: User){
//'在数据返回时设置控件'
tvName?.text = user.name
}
}
复制代码
tvName
被静态地声明在 XML 中,程序动态地经过findViewById()
获取引用,在数据返回的地方调用设值 API。
静态的意味着能够预先定义,且保持不变。而动态的偏偏相反。
对于某个特定的业务场景,除了界面布局是静态的以外,布局中某个控件和哪一个数据绑定也是静态的。这种绑定关系最初是经过“动态”代码实现的,直到出现了Data Binding
。
它是 Google 推出的一种将数据和控件相关联的方法。
若是把 XML 称为声明型的
,那 Kotlin 代码就是程序型的
,前者是静态的,后者是动态的。为了让它俩关联,Data Binding 的思路是把程序型的
变量引入到声明型的
布局中,好比下面把 User.name 绑定到 TextView 上( data_binding_activity.xml ):
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.test.User"/>
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@{user.name}"/>
</layout>
复制代码
其中User
是程序型的实体类:
package com.test
data class User(var name: ObservableField<String>, var age: ObservableField<Int>)
复制代码
ObservableField
用于将任何类型包装成可被观察的对象,当对象值发生变化时,观察者就会被通知。在 Data Binding 中,控件是观察者。
在 Activity 中,这样写代码就完成了数据绑定:
class MainActivity: AppCompatActivity() {
//'声明在 Activity 中的数据源'
private var user:User? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//'为 Activity 设置布局并绑定控件'
val binding = DataBindingUtil.setContentView<DataBindingActivityBinding>(this, R.layout.data_binding_activity);
//'绑定数据源'
binding.user = this.user
}
override fun onUserReturn(user: User){
//'修改数据源'
this.user.name.set( user.name )
}
}
复制代码
这样写的好处是,Activity 中不会再出现findViewById()
和各类为控件设置属性的方法,而只须要观察数据源的变更。
回顾下上一篇构建布局的 DSL :
class MainActivity : AppCompatActivity() {
//'构建布局'
private val rootView by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
TextView {
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 30f
textStyle = bold
align_vertical_to = parent_id
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//'将布局设置为 Activity 的 content view'
setContentView(rootView)
}
}
复制代码
Activity content view 的根布局是 ConstraintLayout,其中包含一个 TextView。为了让它的值和User.name
联动,新增扩展属性以下:
inline var TextView.bindText: LiveData<CharSequence>?
get() {
return null
}
set(value) {
//'为 TextView 的 text 属性绑定数据源'
observe(value) { text = it }
}
//'为控件绑定 LiveData 类型的数据源'
fun <T> View.observe(liveData: LiveData<T>?, action: (T) -> Unit) {
(context as? LifecycleOwner)?.let { owner ->
liveData?.observe(owner, Observer { action(it) })
}
}
复制代码
为 View 新增一个扩展方法,用于在 View 生命周期内观察数据源 LiveData 的变化。当数据源发生变化时执行action
。
而后就能够像这样为 TextView 绑定数据源了:
class MainActivity : AppCompatActivity() {
private val nameLiveData = MutableLiveData<CharSequence>()
private val rootView by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
TextView {
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 30f
textStyle = bold
//'绑定数据源'
bindText = nameLiveData
align_vertical_to = parent_id
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(rootView)
}
override fun onUserReturn(user: User){
//'数据源变动'
nameLiveData.value = user.name
}
}
复制代码
布局 DSL 和界面类定义在同一个kt
文件中,因此它能方便地访问到各类数据源。
把数据源都抽象为LiveData<T>
, 控件中每个须要绑定数据的属性,均可觉得其扩展一个bindXXX
属性,它的值是LiveData<T>
。
上面的例子虽然运用了数据绑定
和LiveData
,但仍是沿用了MVP
的架构。在业务逻辑更复杂的场景,MVP
架构下的 Activity 类就会变得愈来愈臃肿。
再来看一个业务逻辑更复杂的例子。不一样性别的用户名称有不一样颜色:
class MainActivity : AppCompatActivity() {
//'应该让 ViewModel 持有 LiveData'
private val nameLiveData = MutableLiveData<CharSequence>()
//'构建布局应该在 Activity 层完成'
private val rootView by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
TextView {
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 30f
textStyle = bold
//'数据源和控件的绑定应该在Activity层完成'
bindText = nameLiveData
align_vertical_to = parent_id
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(rootView)
}
override fun showUserName(user: User){
//'数据源变动(这段业务逻辑应该写在 ViewModel 里面)'
nameLiveData.value = SpannableStringBuilder(user.name).apply {
setSpan(ForegroundColorSpan(Color.RED), 0, it.name.indexOf(" "), Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
val color = if (user.gender == 1) "#b300ff00" else "#b3ff00ff"
setSpan(ForegroundColorSpan(Color.parseColor(color)), user.name.indexOf(" "), user.name.lastIndex + 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
}
}
}
复制代码
这里的业务逻辑是:用户的姓和名之间会以空格分隔,将用户的姓展现为红色,而名随性别的变化而变色。
demo 是按照 MVP 来写的,但没有展现全部架构的细节,用文字补充以下:
showUserName()
通知界面刷新(由 Activity 实现)。如有了数据绑定,则能够把 Presenter 和 Activity 间通讯都去掉,这也正是 MVP 模式被诟病的地方,即 View 层接口会随着业务逻辑而膨胀。
若改用 MVVM 架构从新实现一边 demo,应该是这样的:
这样 Activity 里面就再也不有 findViewById 和 业务逻辑代码。
想要扩展这套 “DSL + 数据绑定” 方案也极为方便,好比为 ImageView 添加一个绑定 url 的属性:
inline var ImageView.bindSrc: LiveData<Bitmap>?
get() {
return null
}
set(value) {
observe(value) { setImageBitmap(it) }
}
复制代码
先为 ImageView 控件扩展一个名为bindSrc
的属性,它是LiveData<Bitmap>?
类型的。
而后在 构建布局 DSL 中就能够这样使用该属性(为简单起见仍是用 MVP):
class FirstFragment : Fragment() {
//'数据源'
val avatarLiveData = MutableLiveData<Bitmap>()
//'数据源发生变动'
private val target = object : SimpleTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap?, glideAnimation: GlideAnimation<in Bitmap>?) {
avatarLiveData.value = resource
}
}
//构建布局
private val rootView by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
ImageView {
layout_width = 40
layout_height = 40
//'和数据源绑定'
bindSrc = avatarLiveData
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return rootView
}
override fun showUser(user: User){
//'触发加载数据源'
Glide.with(context).load(user.url).asBitmap().into(target)
}
}
复制代码
相较于 DataBinding 中自定义 BindingAdapter 更简单一丢丢。