鸡你太美之 Kotlin 和 Databinding

5,309 阅读5分钟

全民制作人大家好,我是练习时长两年半的个人练习生。喜欢唱、跳、Rap、篮球。

编不下去了... 其实就是之前的一些项目采用了 Databinding,后面考虑用 Kotlin 重新写一遍,特此记录过程中一些比较 tricky 的点。

本文假设读者已经具备一定的 databinding 和 Kotlin 语法基础。

引入

databinding 的引入需要在 app 模块下的 build.gradle 中加入:

android {
    dataBinding.enabled true
    ...
}

同时为了 Kotlin 能够正常使用 databinding 相关的注解,需要同时在 build.gradle 中引入相应插件:

apply plugin: 'kotlin-kapt'

android {
    dataBinding.enabled true
    ...
}

BindingAdapter

我们知道,在 databinding 中,我们经常会使用 BindingAdapter 来为 widget 添加更多的自定义属性,从而以更丰富的手段来将数据绑定到 widget 上。

一般地,比如我们在为 View 设置可见性时,以 Java 编写的话,会有如下的代码:

public class MyBindingAdapter() {
    @BindingAdpater("visible")
    public static setVisible(View v, boolean visible) {
        v.setVisibility(visible ? View.VISIBLE : View.GONE);
    }
}

用 Koltin 编写的话,要省事许多:

@BindingAdapter("visible")
fun setVisible(v: View?, visible: Boolean) {
    v?.visibility = if (visible) View.VISIBLE else View.GONE
}

或者可以直接将该方法作为控件的扩展方法:

@BindingAdapter("visible")
fun View.setVisible(visible: Boolean) {
    this.visibility = if (visible) View.VISIBLE else View.GONE
}

Kotlin 编写的话,不需要多余的类,也不需要多余的静态声明,同时更具有可读性。

ObservableField

我们知道,对于绑定到 layout 中的数据,在更新之后,需要调用 notifyPropertyChanged 来触发 UI 更新。

比如下面这样一个 layout,引用了两个数据字段:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
         <variable
            name="id"
            type="Integer" />

        <variable
            name="name"
            type="String" />
    </data>
    
    <FrameLayout></FrameLayout>
</layout>

对应的 Model 的实现,Java 形式如下:

public class UserVM extends BaseObservable {

    private int id = 0;
    private String name = "";

    @Bindable
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
        notifyPropertyChanged(BR.id);
    }

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

}

Kotlin 利用 Property 的语法糖可以稍微简洁一些:

class UserVM : BaseObservable() {

    @get:Bindable
    var id = 0
        set(id) {
            field = id
            notifyPropertyChanged(BR.id)
        }

    @get:Bindable
    var name = ""
        set(name) {
            field = name
            notifyPropertyChanged(BR.name)
        }

}

但很多时候,我们不想做到单独每个字段都在 <data></data> 中去声明一次,更多地,我们想绑定 UserVM 即可,其字段可以用诸如 @{user.id}@{user.name} 等表示:

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
         <variable
            name="user"
            type="com.packagename.appname.vm.UserVM" />
    </data>
    
    <TextView
        android:width="wrap_content"
        android:height="wrap_content"
        android:text="@{user.name}"/>
        
</layout>

这样一来,notify UserVM 更新就不能让 layout 中使用到 name 字段的 UI 更新,所以在这种场景下我们一般会使用 ObservableField 来单独对字段小粒度实现:

public class UserVM extends BaseObservable {

    public ObservableField<Integer> id = new ObservableField<>(0);

    public ObservableField<String> name = new ObservableField<>("");

}

只要直接修改 ObservableField 的值,就可以触发 UI 更新:

id.set(10);
name.set("Bob");

上面的实现以 Kotlin 编写的话,如下:

class UserVM : BaseObservable() {

    var id = ObservableField(0)

    var name = ObservableField("")

}

修改的话,跟 Java 类似:

id.set(10)
name.set("Bob")

这时候有人问了,“啊那这样 layout 那边拿到的不是 ObservableField 类型的吗,那是不是 widget 在使用的时候,是不是会自动执行一次 toString 将对象转换成字符串,那这样返回的就是对象的 hash 值了,会有问题的。”

其实不然,databinding 在这块对 ObservableField 做过处理,存在 boxunbox 的行为,就有点像 Integer 对象和 int 一样,读者有兴趣的话,可以自行再去深入了解下。

ObservableField 优化

留意到,每个字段都得写长长的 ObservableField 和无谓的默认值,这是一个可优化的地方。

创建 ComOb 类,继承 ObservableField,同时实现几个较常用的类型:

open class ComOb<T>(defaul : T?) : ObservableField<T>() {

    class String(default: kotlin.String = "") : ComOb<kotlin.String>(default)
    
    class Int(default: kotlin.Int = 0) : ComOb<kotlin.String>(default)
    
    class Boolean(default: kotlin.Boolean = false) : ComOb<kotlin.Boolean>(default)
    
}

这样的话,我们在声明时,就可以更加简洁:

class UserVM : BaseObservable() {
    var id = ComOb.Int()
    var name = ComOb.String()
}

同时,我们留意到,Kotlin 对于 ObservableField 的值的修改方式,还是不够 Kotlin 化。诸如 Java 中的 view.setVisibility(xxx) 在 Kotlin 中已经被统一改造为 view.visibility = xxx。Kotlin 的设计是更偏向于属性驱动,而非事件驱动。 //个人理解,不喜勿喷 :)

那么,我们有没有改造的可能?是有的,借助 Kotlin Property 的特性,我们可以做到:

open class ComOb<T>(defaul : T?) : ObservableField<T>() {

    var value: T? = default
        set(value) {
            field = value
            this.set(value)     //注意,这个this.set才是ObservableField原有的方法,即我们之前直接调用的方法
        }

    class String(default: kotlin.String = "") : ComOb<kotlin.String>(default)
    
    class Int(default: kotlin.Int = 0) : ComOb<kotlin.String>(default)
    
    class Boolean(default: kotlin.Boolean = false) : ComOb<kotlin.Boolean>(default)
    
}

这里的做法有点 tricky,是在 ComOb 中制造了一个“傀儡”属性 value,然后将其以 property 的形式暴露出去。

这样一来,我们就可以通过修改 value 来修改 ObservableField 的内部数值(以修改value的间接方式):

class UserVM : BaseObservable() {
    var id = ComOb.Int()
    var name = ComOb.String()
    
    fun foo() {
        id.value = 10   //相当于 id.set(10)
        name.value = "Bob"  //相当于 name.set("Bob")
    }
}

总结

话就说这么多了,好久没写文章,后续希望能够多将实际开发和优化中遇到的问题和解决办法分享给大家。

嗯?所以跟鸡你太美有什么关系???



———————————————

个人博客:mindjet.github.io

最近在 Github 上搞事的项目:

  • LiteWeather [一款用 Kotlin 编写,基于 MD 风格的轻量天气 App],对使用 Kotlin 进行实际开发感兴趣的同学可以看看,项目中会使用到 Kotlin 的委托机制、扩展机制和各种新奇的玩意。
  • Oros [闲来无事做的守望先锋英雄展示 App]
  • LiteReader [一款基于 MD 的极轻阅读 App,提供知乎日报、豆瓣电影等资源],项目主要使用了 MVVM 设计模式,界面遵循 Material Design 规范,提供轻量的阅读体验。

欢迎 star(唱)/ fork(跳)/ issue(rap)/ PR(篮球)