Android官方架構組件DataBinding-Ex: 雙向綁定篇

前言

本文是 Android官方架構組件 系列的番外篇,由於目前國內關於DataBinding雙向綁定的博客,講的實在是五花八門,不少文章看完以後仍然一頭霧水,特此專門寫一篇文章進行總結。java

此外,前幾天在CSDN上看到 貌似掉線 老師發佈了一篇文章《我爲何放棄在項目中使用Data Binding》,裏面針對性指出了目前DataBinding的使用中一些痛點,不少地方我感同身受,但鑑於 事物的存在必然存在兩面性 ,特此也在 本文的末尾 寫了一些我我的的理解, 闡述了爲何我我的 還在堅持使用DataBinding , 但願對讀者能有所裨益。android

本文默認讀者對DataBinding的使用有了初步的瞭解。git

什麼是雙向綁定?

DataBinding的自己是對View層狀態的一種觀察者模式的實現,經過讓ViewViewModel層可觀察的對象(好比LiveData)進行綁定,當ViewModel層數據發生變化,View層也會自動進行UI的更新。github

上述我講的是DataBinding最基礎的用法,即 單向綁定 ,其優點在於,將View層抽象爲一個純Java的可觀察者——這意味着ViewModel層相關代碼是徹底可直接用於進行 單元測試編程

但實際的開發中,單向綁定並不是是足夠的,在一些特定的場景,咱們也須要用到 雙向綁定網絡

好比說,對於一個TextView的內容展現,通常狀況下,咱們只是用來經過將一個String類型的數據對其進行渲染:架構

顯而易見,數據的流向是單向的,換句話說,咱們認爲TextViewDataSource只進行了 操做——若是此時進行了網絡請求,咱們須要用到DataSource某個屬性做爲參數,咱們依然能夠毫無顧忌從DataSource取值。app

可是換一個場景,若是咱們把TextView換成一個EditText,接下來咱們須要面對的則大相徑庭,好比登陸界面: 框架

這彷佛沒有什麼問題,咱們依然經過一個LiveDataEditText進行了單向綁定:源碼分析

問題發生了,當咱們對 輸入框 進行編輯,EditText的UI發生了變動,可是LiveData內的數據卻沒有更新,當咱們想要在ViewModel層請求登陸的API接口時,咱們就必需要去經過editText.getText()才能獲取用戶輸入的密碼。

因而咱們但願,即便是EditText的內容發生了變動,可是LiveData內的數據也能和EditText保持內容的同步——這樣咱們就不須要讓ViewModel層持有View層的引用,在請求接口時,直接從LiveData中取值便可:

這就是雙向綁定的意義。

使用場景是什麼

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

對於單向綁定來講,數據的流向是單向的,換句話說,咱們認爲TextViewDataSource只進行了 操做。

如今咱們定義,當 不肯定的操做發生時 ——一般,這種操做表明着用戶對UI控件的交互,這時UI的變化須要影響到ViewModel層的數據狀態(除了 數據驅動視圖 以外,視圖也在驅動數據,以方便做爲參數未來進行網絡請求等等操做),這時 雙向綁定 就能夠大展身手了。

顯然上文中的EditText的是 雙向綁定 經典的使用場景之一,此外,雙向綁定的使用場景很是常見,好比CheckBox

當用戶選中了CheckBox,咱們固然但願ViewModel層的LiveData<Boolean>狀態進行對應的更新,以便未來咱們直接從LiveData中取值做爲參數進行網絡請求。

而若是沒有雙向綁定,用戶操做了UI,咱們就須要 手動添加代碼保證狀態的同步——好比checkBox.setOnCheckChangedListener(),不然,就會在接下來的操做中獲得與預期不一樣的結果。

聽起來好像很麻煩,那麼究竟如何使用呢?

幸運的是,Android原生控件中,絕大多數的雙向綁定使用場景,DataBinding都已經幫咱們實現好了:

這意味着咱們並不須要去手動實現複雜的雙向綁定,以上文的EditText爲例,咱們只須要經過@={表達式}進行雙向的綁定:

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

相比單向綁定,只須要多一個=符號,就能保證View層和ViewModel層的 狀態同步 了。

難點在哪?

雙向綁定定義好以後,使用起來很簡單,但定義卻稍微比單向綁定麻煩一些,即便原生的控件DataBinding已經幫助咱們實現好了,對於三方的控件或者自定義控件,還須要咱們本身實現

本文以SwipeRefreshLayout爲例,讓咱們來看看其 雙向綁定 實現的方式:

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()
            }
    }
}
複製代碼

有點晦澀,是否是?咱們先不要糾結於細節的實現,先來看看代碼中是如何使用的吧:

<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實際就只是一個LiveData

val refreshing: MutableLiveData<Boolean> = MutableLiveData()
複製代碼

這裏的雙向綁定,意義在於,當咱們爲LiveData手動設置值時,SwipeRefreshLayout的UI也會發生對應的變動;同理,當用戶手動下拉執行刷新操做時,LiveData的值也會對應的變成爲true(表明刷新中的狀態)。

相比於其它的方式,雙向綁定將SwipeRefreshLayout的刷新狀態抽象成爲了一個LiveData<Boolean> ——咱們只須要在xml中定義好,以後就能夠在ViewModel中圍繞這個狀態進行代碼的編寫,不一樣於view.setOnRefreshListener()的方式,這種代碼是純Java的,咱們能夠針對每一行代碼進行純JVM的單元測試。

本小節的全部代碼你均可以在 這裏 獲取。

整理思路,循序漸進實現雙向綁定

說了這麼多,可是咱們一行代碼都尚未實現,不着急,由於編碼只是其中的一個步驟,最重要的是 整理一個流暢的思路,這樣,在接下來的編碼階段,你會若有神助。

1.實現單向綁定

咱們知道,雙向綁定的前提是單向綁定,所以,咱們先配置好對應單向綁定的接口:

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

咱們經過將LiveData的值和DataBinding綁定在一塊兒,每當LiveData的狀態發生了變動,SwipeRefreshLayout的刷新狀態也會發生對應的更新。

咱們實現了數據驅動視圖的效果,接下來咱們須要思考的是,咱們如何才能知道用戶會執行下拉操做呢?

2.觀察View層的狀態變動

只有觀察到View層的狀態變動,咱們才能驅動LiveData進行對應的更新,其實很簡單,經過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的地方,每當swipeRefreshLayout刷新狀態被用戶的操做改變,咱們都可以在這裏監聽到,並交給InverseBindingListener這個 信使 去通知DataBinding

嗨!View層的狀態發生了變動,你快去通知LiveData也進行對應數據的更新呀!

新的問題來了,如今DataBinding已經知道須要去通知LiveData進行對應數據的更新了,關鍵是——

3. 我要把什麼數據交給LiveData?

是的,即便LiveData須要進行更新,可是它並不知道要新的狀態是什麼。

LiveData: 老哥,你卻是把數據給我啊!

咱們急需將SwipeRefreshLayout最新狀態告訴LiveData,所以咱們經過InverseBindingAdapter註解和 步驟二 中去進行對接:

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

注意到 //2 註釋的那行代碼沒有,咱們經過相同的tag(即app:bind_swipeRefreshLayout_refreshingAttrChanged這個字符串,步驟二中咱們也聲明瞭相同的字符串),和 步驟二 中的代碼塊造成了綁定對接。

如今,LiveData知道如何進行反向的數據更新了:

每當用戶下拉刷新,InverseBindingListener通知DataBinding,LiveData就會從swipeRefreshLayout.isRefreshing得知最新的狀態,並進行數據的同步更新。

4.不要忘了防止死循環!

細心的你多少已經感受到了不對勁的地方,如今的雙向綁定有一個致命的問題,那就是無限循環會致使的ANR異常。

View層UI狀態被改變,ViewModel對應發生更新,同時,這個更新又回通知View層去刷新UI,這個刷新UI的操做又會通知ViewModel去更新.......

所以,爲了保證不會無限的死循環致使App的ANR異常的發生,咱們須要在最初的代碼塊中加一個判斷,保證,只有View狀態發生了變動,纔會去更新UI:

@JvmStatic
@BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
fun setSwipeRefreshLayoutRefreshing( swipeRefreshLayout: SwipeRefreshLayout, newValue: Boolean ) {
    if (swipeRefreshLayout.isRefreshing != newValue)   // 只有新老狀態不一樣才更新UI
        swipeRefreshLayout.isRefreshing = newValue
}
複製代碼

小結:我爲何還在堅守DataBinding

本文的初始計劃中,還有一個模塊是關於 雙向綁定的源碼分析,寫到後來又以爲沒有必要了,由於即便是 源碼,也只是將上文中實現的思路囉嗦複述了一遍而已。

雙向綁定自己是一個極具爭議的功能;事實上,DataBinding自己也極具爭議——DataBinding的好用與否,用或者不用都不重要,重要的是咱們須要去正視它展示出來的思想:即如何將一個 難以測試,狀態多變 的View, 經過代碼抽象爲 易於維護和測試 的純Java的狀態?

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

固然,DataBinding並不必定就是正解,事實上,RxBinding就是另一個優秀的解決方案,一樣以SwipeRefreshLayout爲例,我依然能夠將其抽象爲一個可觀察的Observable<Boolean>——前者經過在xml中對數據進行綁定和觀察,後者經過RxJava對View的狀態抽象爲一個流,但最終,二者在思想上異曲同工。

系列文章

爭取打造 Android Jetpack 講解的最好的博客系列

Android Jetpack 實戰篇


關於我

Hello,我是卻把清梅嗅,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人我的博客或者Github

若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章
相關標籤/搜索