Android官方架構(gòu)組件DataBinding-Ex: 雙向綁定篇

前言

本文是 Android官方架構(gòu)組件 系列的番外篇,因?yàn)槟壳皣?guó)內(nèi)關(guān)于DataBinding雙向綁定的博客,講的實(shí)在是五花八門(mén),很多文章看完之后仍然一頭霧水,特此專門(mén)寫(xiě)一篇文章進(jìn)行總結(jié)。

此外,前幾天在CSDN上看到 貌似掉線 老師發(fā)布了一篇文章《我為什么放棄在項(xiàng)目中使用Data Binding》,里面針對(duì)性指出了目前DataBinding的使用中一些痛點(diǎn),很多地方我感同身受,但鑒于 事物的存在必然存在兩面性 ,特此也在 本文的末尾 寫(xiě)了一些我個(gè)人的理解, 闡述了為什么我個(gè)人 還在堅(jiān)持使用DataBinding , 希望對(duì)讀者能有所裨益。

本文默認(rèn)讀者對(duì)DataBinding的使用有了初步的了解。

什么是雙向綁定?

DataBinding的本身是對(duì)View層狀態(tài)的一種觀察者模式的實(shí)現(xiàn),通過(guò)讓ViewViewModel層可觀察的對(duì)象(比如LiveData)進(jìn)行綁定,當(dāng)ViewModel層數(shù)據(jù)發(fā)生變化,View層也會(huì)自動(dòng)進(jìn)行UI的更新。

上述我講的是DataBinding最基礎(chǔ)的用法,即 單向綁定 ,其優(yōu)勢(shì)在于,將View層抽象為一個(gè)純Java的可觀察者——這意味著ViewModel層相關(guān)代碼是完全可直接用于進(jìn)行 單元測(cè)試。

但實(shí)際的開(kāi)發(fā)中,單向綁定并非是足夠的,在一些特定的場(chǎng)景,我們也需要用到 雙向綁定

比如說(shuō),對(duì)于一個(gè)TextView的內(nèi)容展示,一般情況下,我們只是用來(lái)通過(guò)將一個(gè)String類型的數(shù)據(jù)對(duì)其進(jìn)行渲染:

顯而易見(jiàn),數(shù)據(jù)的流向是單向的,換句話說(shuō),我們認(rèn)為TextView對(duì)DataSource只進(jìn)行了 操作——如果此時(shí)進(jìn)行了網(wǎng)絡(luò)請(qǐng)求,我們需要用到DataSource某個(gè)屬性作為參數(shù),我們依然可以毫無(wú)顧忌從DataSource取值。

但是換一個(gè)場(chǎng)景,如果我們把TextView換成一個(gè)EditText,接下來(lái)我們需要面對(duì)的則截然不同,比如登錄界面:

這似乎沒(méi)有什么問(wèn)題,我們依然通過(guò)一個(gè)LiveData對(duì)EditText進(jìn)行了單向綁定:

問(wèn)題發(fā)生了,當(dāng)我們對(duì) 輸入框 進(jìn)行編輯,EditText的UI發(fā)生了變更,但是LiveData內(nèi)的數(shù)據(jù)卻沒(méi)有更新,當(dāng)我們想要在ViewModel層請(qǐng)求登錄的API接口時(shí),我們就必須要去通過(guò)editText.getText()才能獲取用戶輸入的密碼。

于是我們希望,即使是EditText的內(nèi)容發(fā)生了變更,但是LiveData內(nèi)的數(shù)據(jù)也能和EditText保持內(nèi)容的同步——這樣我們就不需要讓ViewModel層持有View層的引用,在請(qǐng)求接口時(shí),直接從LiveData中取值即可:

這就是雙向綁定的意義。

使用場(chǎng)景是什么

什么適合使用 雙向綁定 呢,還記得上文中的一句話嗎:

對(duì)于單向綁定來(lái)說(shuō),數(shù)據(jù)的流向是單向的,換句話說(shuō),我們認(rèn)為TextView對(duì)DataSource只進(jìn)行了 操作。

現(xiàn)在我們定義,當(dāng) 不確定的操作發(fā)生時(shí) ——通常,這種操作代表著用戶對(duì)UI控件的交互,這時(shí)UI的變化需要影響到ViewModel層的數(shù)據(jù)狀態(tài)(除了 數(shù)據(jù)驅(qū)動(dòng)視圖 之外,視圖也在驅(qū)動(dòng)數(shù)據(jù),以方便作為參數(shù)將來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求等等操作),這時(shí) 雙向綁定 就可以大展身手了。

顯然上文中的EditText的是 雙向綁定 經(jīng)典的使用場(chǎng)景之一,此外,雙向綁定的使用場(chǎng)景非常常見(jiàn),比如CheckBox

當(dāng)用戶選中了CheckBox,我們當(dāng)然希望ViewModel層的LiveData<Boolean>狀態(tài)進(jìn)行對(duì)應(yīng)的更新,以便將來(lái)我們直接從LiveData中取值作為參數(shù)進(jìn)行網(wǎng)絡(luò)請(qǐng)求。

而如果沒(méi)有雙向綁定,用戶操作了UI,我們就需要 手動(dòng)添加代碼保證狀態(tài)的同步——比如checkBox.setOnCheckChangedListener(),否則,就會(huì)在接下來(lái)的操作中得到與預(yù)期不同的結(jié)果。

聽(tīng)起來(lái)好像很麻煩,那么究竟如何使用呢?

幸運(yùn)的是,Android原生控件中,絕大多數(shù)的雙向綁定使用場(chǎng)景,DataBinding都已經(jīng)幫我們實(shí)現(xiàn)好了:

這意味著我們并不需要去手動(dòng)實(shí)現(xiàn)復(fù)雜的雙向綁定,以上文的EditText為例,我們只需要通過(guò)@={表達(dá)式}進(jìn)行雙向的綁定:

<EditText
    android:id="@+id/etPassword"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={ fragment.viewModel.password }" />

相比單向綁定,只需要多一個(gè)=符號(hào),就能保證View層和ViewModel層的 狀態(tài)同步 了。

難點(diǎn)在哪?

雙向綁定定義好之后,使用起來(lái)很簡(jiǎn)單,但定義卻稍微比單向綁定麻煩一些,即使原生的控件DataBinding已經(jīng)幫助我們實(shí)現(xiàn)好了,對(duì)于三方的控件或者自定義控件,還需要我們自己實(shí)現(xiàn)

本文以SwipeRefreshLayout為例,讓我們來(lái)看看其 雙向綁定 實(shí)現(xiàn)的方式:

object SwipeRefreshLayoutBinding {

    @JvmStatic
    @BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
    fun setSwipeRefreshLayoutRefreshing(
            swipeRefreshLayout: SwipeRefreshLayout,
            newValue: Boolean
    ) {
        if (swipeRefreshLayout.isRefreshing != newValue)
            swipeRefreshLayout.isRefreshing = newValue
    }

    @JvmStatic
    @InverseBindingAdapter(
            attribute = "app:bind_swipeRefreshLayout_refreshing",
            event = "app:bind_swipeRefreshLayout_refreshingAttrChanged"
    )
    fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =
            swipeRefreshLayout.isRefreshing

    @JvmStatic
    @BindingAdapter(
            "app:bind_swipeRefreshLayout_refreshingAttrChanged",
            requireAll = false
    )
    fun setOnRefreshListener(
            swipeRefreshLayout: SwipeRefreshLayout,
            bindingListener: InverseBindingListener?
    ) {
        if (bindingListener != null)
            swipeRefreshLayout.setOnRefreshListener {
                bindingListener.onChange()
            }
    }
}

有點(diǎn)晦澀,是不是?我們先不要糾結(jié)于細(xì)節(jié)的實(shí)現(xiàn),先來(lái)看看代碼中是如何使用的吧:

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:bind_swipeRefreshLayout_refreshing="@={ fragment.viewModel.refreshing }">

            <androidx.recyclerview.widget.RecyclerView/>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

refreshing實(shí)際就只是一個(gè)LiveData

val refreshing: MutableLiveData<Boolean> = MutableLiveData()

這里的雙向綁定,意義在于,當(dāng)我們?yōu)?code>LiveData手動(dòng)設(shè)置值時(shí),SwipeRefreshLayout的UI也會(huì)發(fā)生對(duì)應(yīng)的變更;同理,當(dāng)用戶手動(dòng)下拉執(zhí)行刷新操作時(shí),LiveData的值也會(huì)對(duì)應(yīng)的變成為true(代表刷新中的狀態(tài))。

相比于其它的方式,雙向綁定將SwipeRefreshLayout的刷新?tīng)顟B(tài)抽象成為了一個(gè)LiveData<Boolean> ——我們只需要在xml中定義好,之后就可以在ViewModel中圍繞這個(gè)狀態(tài)進(jìn)行代碼的編寫(xiě),不同于view.setOnRefreshListener()的方式,這種代碼是純Java的,我們可以針對(duì)每一行代碼進(jìn)行純JVM的單元測(cè)試。

本小節(jié)的所有代碼你都可以在 這里 獲取。

整理思路,按部就班實(shí)現(xiàn)雙向綁定

說(shuō)了這么多,但是我們一行代碼都還沒(méi)有實(shí)現(xiàn),不著急,因?yàn)榫幋a只是其中的一個(gè)步驟,最重要的是 整理一個(gè)流暢的思路,這樣,在接下來(lái)的編碼階段,你會(huì)如有神助。

1.實(shí)現(xiàn)單向綁定

我們知道,雙向綁定的前提是單向綁定,因此,我們先配置好對(duì)應(yīng)單向綁定的接口:

@JvmStatic
@BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
fun setSwipeRefreshLayoutRefreshing(
        swipeRefreshLayout: SwipeRefreshLayout,
        newValue: Boolean
) {
        swipeRefreshLayout.isRefreshing = newValue
}

我們通過(guò)將LiveData的值和DataBinding綁定在一起,每當(dāng)LiveData的狀態(tài)發(fā)生了變更,SwipeRefreshLayout的刷新?tīng)顟B(tài)也會(huì)發(fā)生對(duì)應(yīng)的更新。

我們實(shí)現(xiàn)了數(shù)據(jù)驅(qū)動(dòng)視圖的效果,接下來(lái)我們需要思考的是,我們?nèi)绾尾拍苤烙脩魰?huì)執(zhí)行下拉操作呢?

2.觀察View層的狀態(tài)變更

只有觀察到View層的狀態(tài)變更,我們才能驅(qū)動(dòng)LiveData進(jìn)行對(duì)應(yīng)的更新,其實(shí)很簡(jiǎn)單,通過(guò)swipeRefreshlayout.setOnRefreshListener()即可:

@JvmStatic
@BindingAdapter(
        "app:bind_swipeRefreshLayout_refreshingAttrChanged",
        requireAll = false
)
fun setOnRefreshListener(
        swipeRefreshLayout: SwipeRefreshLayout,
        bindingListener: InverseBindingListener?
) {
    if (bindingListener != null)
        swipeRefreshLayout.setOnRefreshListener {
            bindingListener.onChange()   // 1
        }
}

注意我注釋了 //1的地方,每當(dāng)swipeRefreshLayout刷新?tīng)顟B(tài)被用戶的操作改變,我們都能夠在這里監(jiān)聽(tīng)到,并交給InverseBindingListener這個(gè) 信使 去通知DataBinding

嗨!View層的狀態(tài)發(fā)生了變更,你快去通知LiveData也進(jìn)行對(duì)應(yīng)數(shù)據(jù)的更新呀!

新的問(wèn)題來(lái)了,現(xiàn)在DataBinding已經(jīng)知道需要去通知LiveData進(jìn)行對(duì)應(yīng)數(shù)據(jù)的更新了,關(guān)鍵是——

3. 我要把什么數(shù)據(jù)交給LiveData?

是的,即使LiveData需要進(jìn)行更新,但是它并不知道要新的狀態(tài)是什么。

LiveData: 老哥,你倒是把數(shù)據(jù)給我啊!

我們急需將SwipeRefreshLayout最新?tīng)顟B(tài)告訴LiveData,因此我們通過(guò)InverseBindingAdapter注解和 步驟二 中去進(jìn)行對(duì)接:

@JvmStatic
@InverseBindingAdapter(
        attribute = "app:bind_swipeRefreshLayout_refreshing",
        event = "app:bind_swipeRefreshLayout_refreshingAttrChanged"   // 2 【注意!】
)
fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =
        swipeRefreshLayout.isRefreshing

注意到 //2 注釋的那行代碼沒(méi)有,我們通過(guò)相同的tag(即app:bind_swipeRefreshLayout_refreshingAttrChanged這個(gè)字符串,步驟二中我們也聲明了相同的字符串),和 步驟二 中的代碼塊形成了綁定對(duì)接。

現(xiàn)在,LiveData知道如何進(jìn)行反向的數(shù)據(jù)更新了:

每當(dāng)用戶下拉刷新,InverseBindingListener通知DataBinding,LiveData就會(huì)從swipeRefreshLayout.isRefreshing得知最新的狀態(tài),并進(jìn)行數(shù)據(jù)的同步更新。

4.不要忘了防止死循環(huán)!

細(xì)心的你多少已經(jīng)感覺(jué)到了不對(duì)勁的地方,現(xiàn)在的雙向綁定有一個(gè)致命的問(wèn)題,那就是無(wú)限循環(huán)會(huì)導(dǎo)致的ANR異常。

當(dāng)View層UI狀態(tài)被改變,ViewModel對(duì)應(yīng)發(fā)生更新,同時(shí),這個(gè)更新又回通知View層去刷新UI,這個(gè)刷新UI的操作又會(huì)通知ViewModel去更新.......

因此,為了保證不會(huì)無(wú)限的死循環(huán)導(dǎo)致App的ANR異常的發(fā)生,我們需要在最初的代碼塊中加一個(gè)判斷,保證,只有View狀態(tài)發(fā)生了變更,才會(huì)去更新UI:

@JvmStatic
@BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
fun setSwipeRefreshLayoutRefreshing(
        swipeRefreshLayout: SwipeRefreshLayout,
        newValue: Boolean
) {
    if (swipeRefreshLayout.isRefreshing != newValue)   // 只有新老狀態(tài)不同才更新UI
        swipeRefreshLayout.isRefreshing = newValue
}

小結(jié):我為什么還在堅(jiān)守DataBinding

本文的初始計(jì)劃中,還有一個(gè)模塊是關(guān)于 雙向綁定的源碼分析,寫(xiě)到后來(lái)又覺(jué)得沒(méi)有必要了,因?yàn)榧词故?源碼,也只是將上文中實(shí)現(xiàn)的思路啰嗦復(fù)述了一遍而已。

雙向綁定本身是一個(gè)極具爭(zhēng)議的功能;事實(shí)上,DataBinding本身也極具爭(zhēng)議——DataBinding的好用與否,用或者不用都不重要,重要的是我們需要去正視它展現(xiàn)出來(lái)的思想:即如何將一個(gè) 難以測(cè)試,狀態(tài)多變 的View, 通過(guò)代碼抽象為 易于維護(hù)和測(cè)試 的純Java的狀態(tài)?

DataBinding將煩不勝煩的View層代碼抽象為了易于維護(hù)的數(shù)據(jù)狀態(tài),同時(shí)極大減少了View層向ViewModel層抽象的 膠水代碼,這就是最大的優(yōu)勢(shì)。

當(dāng)然,DataBinding并不一定就是正解,事實(shí)上,RxBinding就是另外一個(gè)優(yōu)秀的解決方案,同樣以SwipeRefreshLayout為例,我依然可以將其抽象為一個(gè)可觀察的Observable<Boolean>——前者通過(guò)在xml中對(duì)數(shù)據(jù)進(jìn)行綁定和觀察,后者通過(guò)RxJava對(duì)View的狀態(tài)抽象為一個(gè)流,但最終,兩者在思想上殊途同歸。

系列文章

爭(zhēng)取打造 Android Jetpack 講解的最好的博客系列

Android Jetpack 實(shí)戰(zhàn)篇


關(guān)于我

Hello,我是卻把清梅嗅,如果您覺(jué)得文章對(duì)您有價(jià)值,歡迎 ??,也歡迎關(guān)注我的個(gè)人博客或者Github。

如果您覺(jué)得文章還差了那么點(diǎn)東西,也請(qǐng)通過(guò)關(guān)注督促我寫(xiě)出更好的文章——萬(wàn)一哪天我進(jìn)步了呢?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容