Android Jetpack——DataBinding:從排斥到真香

好像確實如此

剛學Android Jetpack時,前輩們都不怎麼推薦使用DataBinding。從中瞭解到DataBinding是這樣的:

  • 消除findViewById (我選擇kt)
  • 在xml中寫(邏輯)代碼 (黑人問號面???,反感)
  • 無需手動設置一些監聽 (不就幾個監聽嗎)

soDataBinding在我眼裏作用不大,甚至有點反感(主要xml那塊),很長一段時間都排斥DataBinding,項目中只使用ViewModel和LiveData等其他Jetpack組件。

漸漸入坑

藉助kt的插件,我們在任何地方都不需要寫findViewById(感謝大佬指出)。但由於以前不太懂在RecyclerView.ViewHolder中使用kt插件,還是老老實實的findViewById。這也讓我想起DataBinding的好處:消除findViewById。而且對一個組件學都沒學,在不瞭解的情況下,就判處"死刑",好像也不妥。

於是我決定嘗試學習一下DataBinding,但秉着不在xml中寫邏輯代碼的原則,在學習DataBinding時,有關運算符的介紹都是跳過不看的。例如這些:

減少膠水代碼

原本使用kotlin搬磚的我,減少膠水代碼纔是databinding爲我帶來最直接的便利。比起修改LiveData的值,然後設置Observer感知LiveData的變化,纔對View的數據或狀態進行調整。直接使用DataBinding,修改數據的同時,View的數據或狀態同步修改,更有一氣呵成的感覺。無論是數據綁定,雙向綁定,還是設置監聽,都是在減少膠水代碼。

正如@卻把清梅嗅大佬在Android官方架構組件DataBinding-Ex: 雙向綁定篇中總結到的:

DataBinding將煩不勝煩的View層代碼抽象爲了易於維護的數據狀態,同時極大減少了View層向ViewModel層抽象的 膠水代碼,這就是最大的優勢。

“數據級聯”

很多時候,我們需要對數據進行轉換,以便在前端給用戶展示正確的信息,而這種轉換應該是自發的。衆所周知,在LiveData中存在map擴展方法(內部調用Transformations#map),我們可以利用該函數對數據進行自發的轉換。

//性別
val sex = MutableLiveData<Int>()
val sexStr = sex.map {
    when(it){
        1 -> "男"
        2 -> "女"
        else -> ""
    }
}

我可以照常對sex進行賦值,讓數據轉換自發進行。而數據綁定時,只需要對sexStr進行綁定或訂閱。set原始數據,視圖呈現我需要展示的信息,好像這樣更符合數據驅動的思想。

但由於當時不知道LiveData也能用於dataBinding的數據綁定(流下了沒有技術的眼淚),於是我陷入了使用LiveData無法進行數據綁定,使用Observable*類無法進行自發轉換的“困境”。

我在想:Observable對象也具轉換的能力,該多好呀。於是我打算利用Observable類#addOnPropertyChangedCallback,對Observable類定義一系列map擴展方法。但定義完一個後發現,Observable類數目較多,對其定義map擴展無疑是 n * n 的排列組合(考慮到避免基礎類型的裝箱與拆箱)。懶人的我當然是選擇放棄啦,只能另尋它法。

直到在官網看到這段代碼:

https://developer.android.com/reference/android/databinding/ObservableField

第三個ObservableField的構造方法中,傳入了前兩個ObservableField對象:first 和 last。並且重載了內部的 get 方法, get 方法的值依據first 和 last生成。這好像是我想要的東西!!

查看了一下源碼:

ObservableField.java

/**
 * Creates an ObservableField that depends on {@code dependencies}. Typically,
 * ObservableFields are passed as dependencies. When any dependency
 * notifies changes, this ObservableField also notifies a change.
 *
 * @param dependencies The Observables that this ObservableField depends on.
 */
public ObservableField(Observable... dependencies) {
    super(dependencies);
}

BaseObservableField.java

public BaseObservableField(Observable... dependencies) {
    if (dependencies != null && dependencies.length != 0) {
        DependencyCallback callback = new DependencyCallback();

        for (int i = 0; i < dependencies.length; i++) {
            dependencies[i].addOnPropertyChangedCallback(callback);
        }
    }
}

class DependencyCallback extends Observable.OnPropertyChangedCallback {
    @Override
    public void onPropertyChanged(Observable sender, int propertyId) {
        notifyChange();
    }
}

簡單說就是對構造方法中多個Observable類對象(如 first 和 last)添加一個屬性改變回調Observable.OnPropertyChangedCallback。讓這些Observable類對象在值改變時(例如first的變化值 或 last的值變化時),通過Observable.OnPropertyChangedCallback回調通知一聲(告訴 display:我改變數據了 )。

然後又通過notifyChange()告知其他人,他自身屬性也改變了,讓其他人重新獲取他的值。重寫內部的get方法就能夠讓其他地方重新獲取該值時(你都告訴我,你的值改變了,那我當然要重新獲取一遍啦),計算出新值。

對於這樣的現象:

一個對象擁有感知一個或多個對象的值變化的能力。當其感知對象的值改變時,自動調整自身的值。

暫且叫它“數據級聯”吧

解決xml中的邏輯代碼

回頭一看,那些xml裏編寫的簡單邏輯表達式,不正可以使用“數據級聯”進行完成嗎?以官網的代碼爲例:

android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"

無非就是一個visibility值,需要依賴age的值,進行一些運算得出結果。而且每次age的值更新,需要通知visibility重新進行計算:

val age = ObservableInt()
val visibility = object :ObservableInt(age){
    override fun get(): Int {
        return if (age.get() > 13)  
            View.GONE 
        else 
            View.VISIBLE
    }
}

#xml
android:visibility="@{viewModel.visibility}"

而且以前在xml中無法實現的較爲複雜的邏輯代碼,也可以嘗試通過“數據級聯”來實現。這樣一來,xml層就只剩下數據綁定和設置監聽的代碼,我個人覺得還是可以接受的。(問了下前端的同學,Vue好像也是類似DataBinding這樣綁定的)

實踐

模仿b站的登錄頁面:

只有手機號碼輸入框和驗證碼輸入框都存在輸入值時,登錄按鈕纔可點擊,且透明度跟隨改變。

//手機號碼
val phone = ObservableField<String>()
//驗證碼
val smsCode = ObservableField<String>()

//登錄按鈕可點擊狀態
val loginEnable = object :ObservableBoolean(phone,smsCode) {
    override fun get(): Boolean {
        //獲取手機輸入框內容
        val phoneStr = phone.get()
        //獲取驗證碼輸入框內容
        val smsCodeStr = smsCode.get()
        //手機框和密碼框都存在輸入值時,才允許點擊登錄按鈕
        return if (phoneStr.isNullOrEmpty() || smsCodeStr.isNullOrEmpty())
            false
        else
            true
    }
}
//登錄按鈕的透明度
val loginAlpha = object:ObservableFloat(loginEnable){
    override fun get(): Float {
        //獲取按鈕是否可點擊的布爾值
        val enable = loginEnable.get()
        return if (enable)
            1.0f
        else
            0.5f
    }
}

此時,我們需要做得就是通過DataBinding將這4個Observable*類對象綁定到xml就可以了(phone和smsCode使用雙向綁定)。而這一切,都是自發的,豈不美哉?當然,LiveData也可以使用MediatorLiveData來實現對多個數據源監聽。在這些數據源發生改變時,對其進行通知:

//手機號碼
val phone = MutableLiveData<String>().apply { value = "" }
//驗證碼
val smsCode = MutableLiveData<String>().apply { value = "" }

 //獲取登錄按鈕可點擊狀態
val loginEnable = MediatorLiveData <Boolean>().apply {
    val observer = Observer<String> {
        refreshEnable()
    }

    addSource(smsCode,observer)
    addSource(phone,observer)
}
//登錄按鈕的透明度
val loginAlpha = loginEnable.map { enable ->
    if (enable)
        1.0f
    else
        0.5f
}

fun refreshEnable(){
    //獲取驗證碼
    val smsCodeStr = smsCode.value
    //獲取手機號碼
    val phoneStr = phone.value
    //手機號碼和驗證碼不爲空,纔可以點擊登錄按鈕
    if (phoneStr.isNullOrEmpty() || smsCodeStr.isNullOrEmpty())
        loginEnable.value = false
    else
        loginEnable.value = true
}

自定義BindingAdapter優化

DataBinding具有自動優化View更新的能力,這與官方優化的BindingAdapter有不少關係:

#TextViewBindingAdapter.java
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
    final CharSequence oldText = view.getText();
    //會比較新舊值,一樣則不重新賦值。
    if (text == oldText || (text == null && oldText.length() == 0)) {
        return;
    }
    if (text instanceof Spanned) {
        if (text.equals(oldText)) {
            return; // No change in the spans, so don't set anything.
        }
    } else if (!haveContentsChanged(text, oldText)) {
        return; // No content changes, so don't set anything.
    }
    view.setText(text);
}

很多時候,我們需要將一些數據綁定的重複邏輯抽離到BindingAdapter中(例如ImageView依據url使用Glide加載圖片),但其質量也會存在參差不齊的情況。這需要我們在自定義BindingAdapter時,儘量對值進行一些必要的判斷,以減少View的重新測量與重繪。

在BindingAdapter中攔截沒用的數據來優化View更新,宛如是“末段攔截”。比起 “末段攔截” 更有用的是 “中程攔截”。具有防抖功能的Observable*類比 LiveData更具備 “中程攔截” 的能力,因爲它在每次賦值前都會判斷是否和舊值相等:

public void set(int value) {
    if (value != mValue) {
        mValue = value;
        notifyChange();
    }
}

回傳舊值

DataBinding允許在自定義的BindingAdapter中,回傳舊值。但是需要先聲明舊值,再聲明新值。詳見:DataBinding介紹。但對於能通過公開屬性獲取到的數據,要求DataBinding回傳舊值的作用並不大。反而像監聽、回調這些,可以藉助DataBinding回傳舊值的功能來進行移除。(當然直接替換的監聽/回調也就不需要回傳舊值)

@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
        view: View,
        oldValue: View.OnLayoutChangeListener?,
        newValue: View.OnLayoutChangeListener?
) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue)
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue)
        }
    }
}

使用DataBinding的一點小建議

  • 不在xml中寫任何邏輯代碼,databinding在xml中只負責數據綁定和設置監聽。
  • xml標籤中使用databinding的屬性統一移到標籤底部。
  • 儘量使用官方定義的BindingAdapter進行數據綁定與設置監聽。

小結

DataBinding加速MVVM的構建,減少大部分膠水代碼。沒有DataBinding也可以藉助LiveData和ViewModel構建MVVM。如果實在無法容忍在xml中寫入額外的東西,可以放棄DataBinding。

xml中的代碼應該全部抽離到Java/Kt中,使用"數據級聯"將其進行轉換,再將其綁定到View中。

數據綁定優先選擇Observable類而非 LiveData。如果想對Observable類的值進行監聽,可以使用Observable*類#addOnPropertyChangedCallback添加回調。

DataBinding總體來說還是很香的^ ^

最後其實對於程序員來說,要學習的知識內容、技術有太多太多,要想不被環境淘汰就只有不斷提升自己,從來都是我們去適應環境,而不是環境來適應我們!

 

這裏是關於我自己的Android 學習,面試文檔,視頻收集大整理,有興趣的夥伴們可以看看~

當程序員容易,當一個優秀的程序員是需要不斷學習的,從初級程序員到高級程序員,從初級架構師到資深架構師,或者走向管理,從技術經理到技術總監,每個階段都需要掌握不同的能力。早早確定自己的職業方向,才能在工作和能力提升中甩開同齡人。

發佈了208 篇原創文章 · 獲贊 780 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章