[譯] Data Binding 庫使用的經驗教訓

Mirosalva發表於2019-04-13

由 [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 平臺的使用者 [rawpixel](https://unsplash.com/photos/uQkwbaP0UrI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 拍攝

Data Binding 庫(下文中以『DB 庫』詞語來指代)提供了一個靈活強大的方式來繫結資料到 UI 介面。但是要用一句陳詞濫調:『能力越大,責任越大』,僅僅是使用資料繫結,並不意味著你可以避免成為一個優秀 UI 開發者。

過去的幾年我一直在 Android 開發中使用 data binding 庫,本文會寫出我這一路上了解到的與它有關的一些內容細節。

儘可能使用 bindings

自定義 binding adapter 是一種給 View 控制元件輕鬆提供自定義功能的好方法。和許多開發者一樣,我對 binding adapter 研究得稍微深入,最終總結出一套包含 15 種不同用途的介面卡的類集。

最糟糕的實踐是這類介面卡,它們生成格式化的字串並設定到 TextViews 控制元件,這些介面卡通常僅在同一個佈局檔案中使用:

雖然這可能看起來很聰明,但是有三大缺點:

  1. 優化它們的過程太痛苦。除非你把程式碼組織得非常好,否則你可能會有一個包含所有介面卡方法的大檔案,這與程式碼內聚和解耦原則相違背。

  2. 你需要使用 instrumentation 工具來做測試。根據定義,你的 binding adapter 不會有返回值,它們接收一個輸入引數後設定 view 的屬性。這就意味著你必須使用 instrumentation 來測試你的自定義邏輯,這樣會使得測試變得既緩慢又難以維護。

  3. 自定義 binding adapter 程式碼(通常)不是最佳選項。如果你檢視內建文字繫結[參考這裡],你將會看到已經做了許多檢查來避免呼叫 TextView.setText(),這樣就節省了被浪費的佈局檢測。我覺得自己陷入了這樣的思維困境:DB 庫將會自動優化我的 view 更新。它確實可以做到,但僅限於你使用被謹慎優化的內建 binding adapter的情況。

相反的,把你的方法的邏輯抽象為內聚類(我稱之為文字建立者類),然後將它們傳遞給 binding。這樣你就可以呼叫你的文字建立者類並使用內建 view binding:

這樣我們可以從內建的繫結操作過程中提高效率,並且我們可以非常輕鬆地對建立格式化字串的程式碼進行單元測試。

讓你的自定義 binding 介面卡變得高效

如果你確實需要使用自定義介面卡,因為你所需的功能不存在,請儘量使其變得高效。我的意思是使用所有標準的 Android UI 優化:儘可能避免觸發測量/佈局操作。

這可以像檢查當前使用的檢視以及你設定的內容一樣簡單。這裡有一個我們為 android:drawable 重新實現了標準 ImageView adapter 的樣例:

遺憾的是,檢視並不總是能夠顯示我們需要檢查的狀態。這裡有一個在 TextView 上設定切換最大行的示例。它通過改變 TextView 的 maxLines 屬性以及一個延時佈局轉換(android.view.ViewGroup)來實現切換。

這樣你就可以瞭解它的作用

之前 binding adapter 比較簡單並且總是設定了 maxLines 屬性和一個點選監聽物件。TextView 在 setMaxLines() 被呼叫後總會觸發一次佈局,這就意味著每次 binding adapter 啟動,一次佈局就會被觸發。

讓我們改變這個情況。由於此功能與 TextView 是完全分開的(我們只是在單擊時使用不同的值呼叫 setMaxLines()),我們需要將引用儲存為當前狀態。幸運的是,『DB 庫』為我們提供了一個手工方式去在 binding adapter 中接收狀態。通過提供引數兩次:第一個引數接收當前值,第二個引數接收值。

所以這裡我們只需比較當前的新的 collapsedMaxLines 值。如果值實際發生了改變,我們才去呼叫 setMaxLines() 等方法。

編輯按: 感謝 Alexandre Gianquinto 在評論中提到『double parameters』功能。

謹慎對待你提供的變數

我一直在慢慢的重新設計 Tivi,使用類似 MVI 的東西,使用優秀的 MvRx 庫來使它變得規範化。這在實踐中意味著我的 fragment/view 訂閱到 ViewModel物件,並且接收 ViewStates 的例項。這些例項包含所有用於顯示 UI 的必要狀態。

這是一個展示 Tivi(連結)中類的樣例:

你可以看到它僅僅是一個簡單的資料類,包含了 UI 需要在一個 TV 秀介面上顯示的所有細節 UI 元素。

聽起來像是傳遞我們的 data binding 例項物件的完美選項,讓我們的 binding 表示式來去更新 UI,對吧?好吧這確實有效,但是有一些需要注意的地方,這是由於『DB 庫』的工作機制。

在 data binding 中你通過 <variable> 標籤宣告瞭輸入,然後在書寫 binding 表示式時在 view 屬性處引用了這些輸入變數。當任何被依賴的變數發生變化,『DB 庫』都會執行你的 binding 表示式(接著會更新 view)。這個變化檢測就是你可以免費獲取的很棒的優化。

所以回到我的場景,我的佈局最終看起來是這樣的:

所以我最終獲取一個包含所有 UI 狀態的全域性 ViewState 例項,並且你可以想象出這些狀態經常會發生變化。UI 狀態的任何輕微變化都會產生一個全新的 ViewState,並被傳遞到我們的 data binding 例項。

所以問題是什麼?由於我們只有一個輸入變數,所有的 binding 表示式將會引用變數,這就意味著『DB 庫』將無法自由選擇執行哪個表示式。在實際過程中,這意味著每次變數變化(不管多小的變化)發生時所有的 binding 表示式都會執行。

這個問題與 MVI 這點無關,特別是它只是組合狀態的 artifact,與data binding 結合在一起使用。

那麼你能怎麼做呢?

有種替代方法是在佈局中顯式宣告 ViewState 中的每個變數,然後顯式傳遞組合狀態例項中的值,如下所示:

這顯然會使開發人員維護和同步更多的程式碼,但它確實意味著『DB 庫』可以優化去執行哪些表示式。如果你的 UI 狀態不經常變化(可能在建立時有一些次)並且變數數量較少時,我會推薦使用此模式。

我個人一直在佈局中使用單個變數,傳入我的 ViewState 例項,並依賴於我們的檢視繫結合理地執行。這就是為什麼讓檢視繫結變得高效非常重要。

另一個需要注意的是 Tivi 是 RecyclerView 的重度使用者,還有 EpoxyData Binding,意思就是在 DiffUtil 中會額外有一些變化相關的計算髮生。所以如果你的 UI 也有大量的 RecyclerView 組成,你可以類似上文描述不費事地獲取計算這方面的優化。

小步迭代

希望這篇文章強調了一些可以優化資料繫結實現方案中的一些小事。瞭解『DB 庫』的內部機制可以幫助你提高資料繫結效率,並提高你的 UI 效能。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章