雞你太美之 Kotlin 和 Databinding

Mindjet發表於2019-05-12

全民製作人大家好,我是練習時長兩年半的個人練習生。喜歡唱、跳、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(籃球)

相關文章