前言
本文是 Android官方架構元件 系列的番外篇,因為目前國内關于DataBinding雙向綁定的部落格,講的實在是五花八門,很多文章看完之後仍然一頭霧水,特此專門寫一篇文章進行總結。
此外,前幾天在CSDN上看到 貌似掉線 老師釋出了一篇文章《我為什麼放棄在項目中使用Data Binding》,裡面針對性指出了目前DataBinding的使用中一些痛點,很多地方我感同身受,但鑒于 事物的存在必然存在兩面性 ,特此也在 本文的末尾 寫了一些我個人的了解, 闡述了為什麼我個人 還在堅持使用DataBinding , 希望對讀者能有所裨益。
本文預設讀者對DataBinding的使用有了初步的了解。
什麼是雙向綁定?
DataBinding的本身是對View層狀态的一種觀察者模式的實作,通過讓View與ViewModel層可觀察的對象(比如LiveData)進行綁定,當ViewModel層資料發生變化,View層也會自動進行UI的更新。
上述我講的是DataBinding最基礎的用法,即 單向綁定 ,其優勢在于,将View層抽象為一個純Java的可觀察者——這意味着ViewModel層相關代碼是完全可直接用于進行 單元測試。
但實際的開發中,單向綁定并非是足夠的,在一些特定的場景,我們也需要用到 雙向綁定。
比如說,對于一個TextView的内容展示,一般情況下,我們隻是用來通過将一個String類型的資料對其進行渲染:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIzUTZkBTOhJmNihTZvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
顯而易見,資料的流向是單向的,換句話說,我們認為TextView對DataSource隻進行了 讀 操作——如果此時進行了網絡請求,我們需要用到DataSource某個屬性作為參數,我們依然可以毫無顧忌從DataSource取值。
但是換一個場景,如果我們把TextView換成一個EditText,接下來我們需要面對的則截然不同,比如登入界面:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIzUTZkBTOhJmNihTZvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
這似乎沒有什麼問題,我們依然通過一個LiveData對EditText進行了單向綁定:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIzUTZkBTOhJmNihTZvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
問題發生了,當我們對 輸入框 進行編輯,EditText的UI發生了變更,但是LiveData内的資料卻沒有更新,當我們想要在ViewModel層請求登入的API接口時,我們就必須要去通過editText.getText()才能擷取使用者輸入的密碼。
于是我們希望,即使是EditText的内容發生了變更,但是LiveData内的資料也能和EditText保持内容的同步——這樣我們就不需要讓ViewModel層持有View層的引用,在請求接口時,直接從LiveData中取值即可:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIzUTZkBTOhJmNihTZvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
這就是雙向綁定的意義。
使用場景是什麼
什麼适合使用 雙向綁定 呢,還記得上文中的一句話嗎:
對于單向綁定來說,資料的流向是單向的,換句話說,我們認為TextView對DataSource隻進行了 讀 操作。
現在我們定義,當 不确定的操作發生時 ——通常,這種操作代表着使用者對UI控件的互動,這時UI的變化需要影響到ViewModel層的資料狀态(除了 資料驅動視圖 之外,視圖也在驅動資料,以友善作為參數将來進行網絡請求等等操作),這時 雙向綁定 就可以大展身手了。
顯然上文中的EditText的是 雙向綁定 經典的使用場景之一,此外,雙向綁定的使用場景非常常見,比如CheckBox:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIzUTZkBTOhJmNihTZvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
當使用者選中了CheckBox,我們當然希望ViewModel層的LiveData狀态進行對應的更新,以便将來我們直接從LiveData中取值作為參數進行網絡請求。
而如果沒有雙向綁定,使用者操作了UI,我們就需要 手動添加代碼保證狀态的同步——比如checkBox.setOnCheckChangedListener(),否則,就會在接下來的操作中得到與預期不同的結果。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIzUTZkBTOhJmNihTZvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
聽起來好像很麻煩,那麼究竟如何使用呢?
幸運的是,Android原生控件中,絕大多數的雙向綁定使用場景,DataBinding都已經幫我們實作好了:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIzUTZkBTOhJmNihTZvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
這意味着我們并不需要去手動實作複雜的雙向綁定,以上文的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()
}
}
}
有點晦澀,是不是?我們先不要糾結于細節的實作,先來看看代碼中是如何使用的吧:
android:layout_width="match_parent"
android:layout_height="match_parent"
app:bind_swipeRefreshLayout_refreshing="@={ fragment.viewModel.refreshing }">
refreshing實際就隻是一個LiveData:
val refreshing: MutableLiveData = MutableLiveData()
這裡的雙向綁定,意義在于,當我們為LiveData手動設定值時,SwipeRefreshLayout的UI也會發生對應的變更;同理,當使用者手動下拉執行重新整理操作時,LiveData的值也會對應的變成為true(代表重新整理中的狀态)。
相比于其它的方式,雙向綁定将SwipeRefreshLayout的重新整理狀态抽象成為了一個LiveData ——我們隻需要在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——前者通過在xml中對資料進行綁定和觀察,後者通過RxJava對View的狀态抽象為一個流,但最終,兩者在思想上殊途同歸。
系列文章
關于我
Hello,我是卻把清梅嗅,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的個人部落格或者Github。
如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?