findViewById不香嗎?為什麼要把簡單的問題複雜化?為什麼要用DataBinding?

語言: CN / TW / HK

theme: smartblue highlight: agate


Android-MVVM-Databinding的原理、用法與封裝

前言

說起 DataBinding/ViewBinding 的歷史,可謂是一波三折,甚至是比 Dagger/Hilt 還要傳奇。

說起依賴注入框架 Dagger2/Hilt ,也是比較傳奇,剛出來的時候火的一塌糊塗,各種攻略教程,隨後發現坑多難以使用,隨之逐漸預冷,近幾年在 Hilt 釋出之後越發的火爆了。

而 DataBinding/ViewBinding 作為 Android 官方的親兒子庫,它的經歷卻更加的離奇,從釋出的時候火爆,然後到坑太多直接遇冷,隨之被其他框架替代,再到後面 Kotlin 出來之後是更加的冷門了,全網是一片吐槽,隨著 Kotlin 外掛廢棄之後 ViewBinding 的推出而再度翻火...都夠拍一部大片了。😅

說到這裡了,在Android開發者,特別是沒用過 DataBinding 的開發者心中可能都有一個大致的印象,DataBinding太坑了,太老了,更新慢,都是缺點,跑都跑不起來,狗都不用...😅😅

t01179481d481d4b968.gif

這也是 DataBinding/ViewBinding 框架的發展歷程導致的,幾起幾落結果就給開發者留下了全是缺點這麼個印象。

那麼作為官方主推的 MVVM 架構指定框架 DataBinding 真的有這麼不堪嗎?😂

在目前看來 Android 客戶端開發還沒有進化到 Compose,我們目前的主流佈局方案還是XML,而基於VMMV架構的 DataBinding 框架還是很有必要學習與使用的。💪

老話這麼說,我可以不用,但是我要會。就算自己不用,至少也要能看懂別人的程式碼吧。

閒話不多說,下面就簡單從幾點分析一下,為什麼Googel推薦使用 DataBinding/ViewBinding ,如何使用,以及基本的原理,最後推薦一些 DataBinding 的封裝簡化使用流程。

0LfPrjVgtZ.GIF

一、之前的方案有哪些不足

只要是 Android 開發的從業者,從開始學習起就知道找控制元件的方式是 findViewById,下面先講講它的大致原理。

我們以Activity中使用 findViewById 為例:

androidx.appcompat.app java @Override public <T extends View> T findViewById(@IdRes int id) { return getDelegate().findViewById(id); }

可以看到是通過委派類呼叫的,其實是呼叫到 Window 類中的 findViewById 方法: java public <T extends View> T findViewById(int id) { if (id == NO_ID) { return null; } return findViewTraversal(id); }

內部又呼叫到 ViewGroup 的 findViewTraversal 方法。內部又是遍歷找 id 的邏輯

image.png

如果佈局正好在此 ViewGroup 中那隻遍歷一次,如果巢狀的很深,則會一層一層的遍歷去找 id ,這是會稍稍影響效能的。

並且我們在使用 findViewById 的時候是可能出現的錯誤問題:

  1. 需要強轉的問題。
  2. 呼叫時機錯誤的問題。
  3. 響應式佈局中由於佈局差異導致空指標的問題。
  4. Activity+Fragment架構中,Fragment初始化了但是沒有新增到Activity中導致的問題。
  5. 如果一個Activity中有多個Fragment,Fragment中的控制元件名稱又有重複的,直接使用findViewById會爆錯。
  6. 同樣的問題再Dialog與PopuoWindow都可能存在已初始化但沒新增的問題。
  7. 當前Activity找到其他Activity的相同id,但真實不存在的問題。
  8. 由於重建、恢復導致的控制元件空指標問題。

等等,當然了,其中很多問題是邏輯問題導致的空指標,鍋不能都扣到 findViewById 頭上。就算我們使用其他的包括 DataBinding 的方案時也並不能完全避免空指標的,只能說盡量避免空指標。

這都不說了,關鍵是當佈局中的 ID 很多的時候,需要寫大量的 findViewById 模板程式碼。這簡直是要命了,所以就引申出了很多框架或外掛。

例如 XUtils,ButterKnife,FindViewByMe(外掛)等。

雖然 XUtils,ButterKnife 這類外掛可以專門對 findviewbyid 方法進行簡化,但是還是需要寫註解讓控制元件與資源繫結,當然後期還專門有針對繫結的外掛。

但是其本質還是 findViewById 那一套,再後來隨著元件化與外掛化的火熱,類似 ButterKnife 在這樣的架構中或多或少的有一些其他的問題 R R1 R2...總感覺乖乖的,有點雞肋的意思,用的人也是越來越少了。

而隨著 Kotlin 的流行,和 kotlin-android-extensions 外掛的誕生,一切又不一樣了,開發者也有了新的選擇。

Kotlin 直接從語言層面支援 Null 安全,於是 DataBinding 在 Kotlin 語言的專案中基本上是銷聲匿跡了。

很多人可能就是因為 kotlin-android-extensions 外掛從而使用 Kotlin 的,不需要手動 findviewbyid 了,實在是太爽了。

kotlin-android-extensions 是如何實現的,我們檢視一下 Kotlin Bytecode 的位元組碼:

```java public final class MainActivity extends AppCompatActivity { private HashMap _$_findViewCache;

protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(1300023); TextView var10000 = (TextView)this._$_findCachedViewById(id.textView); var10000.setText((CharSequence)"Hello"); }

public View $_findCachedViewById(int var1) { if (this.$findViewCache == null) { this.$findViewCache = new HashMap(); } View var2 = (View)this.$findViewCache.get(var1); if (var2 == null) { var2 = this.findViewById(var1); this.$_findViewCache.put(var1, var2); } return var2; } } ```

kotlin-android-extensions外掛會幫我們生成一個_$_findCachedViewById()函式,優先從記憶體快取 HashMap 中找控制元件,找不到就會呼叫原生的 findViewById 新增到記憶體快取中,是的,就是我們常用的很簡單的快取邏輯。

image.png

後期的發展大家也知道了,隨著 apply plugin: 'kotlin-android-extensions' 外掛被官方背棄了,至於為什麼被廢棄,我個人大致猜測可能是:

  1. 底層還是基於 findViewById,還是會有 findViewById 的弊端,只是多了快取的處理。
  2. 就算是多了快取看起來很美,但快取並不好用,在部分需要回收再次使用的場景,例如 RV.Adapter.ViewHolder 中存在快取失效每次都 findViewById 而導致的效能問題(還不如不要呢)。
  3. 每一個 Page/Item 都需要一個 HashMap 來儲存 View 例項,佔用記憶體過大。
  4. xml 中的 ID 沒有跟頁面繫結,一樣有 findViewById 的那些問題,在當前 Activity 可以找到其他頁面的 ID。

再而後 2019 年 Google 推出了 ViewBinding 終結一切,如果佈局中的某個 View 例項隱含 Null 安全隱患,則編譯時 ViewBinding 中間程式碼為其生成 @Nullable 註解。從而最大限度避免控制元件的空指標異常。並且由於檢視繫結會建立對檢視的直接引用,因此不存在因檢視的 ID 無效而引發空指標異常。並且每個繫結類中的欄位均具有與它們在 xml 檔案中引用的檢視相匹配的型別。這意味著不存在發生類轉換異常的風險。

而 DataBinding 作為 ViewBinding 的老大哥則又一次登上了舞臺。

image.png

DataBinding VS ViewBinding :兩者都能做 binding UI layouts 的操作,但是 DataBinding 還支援一些額外的功能,如雙向繫結,xml中使用變數等。ViewBinding不會新增編譯時間,而 DataBinding 會新增編譯時間,並且 DataBinding 會少量增加 apk 體積, ViewBinding 不會。總的來說ViewBinding更加的輕量。

題外話:ButterKnife 的作者已經宣佈不維護 ButterKnife,作者推薦使用 ViewBinding 了。

二、ViewBinding/DataBinding如何使用

由於 DataBinding 是與 AGP(Android Gradle 外掛) 捆綁在一起的,所以我們不需要導依賴包,只需要在配置中啟動即可。

老版本定義如下(4.0版本以下): android { viewBinding { enabled = true } dataBinding{ enabled = true } }

新版本定義如下(4.0版本以上): android { buildFeatures { dataBinding = true viewBinding = true } }

配置完成之後在我們的xml根佈局標籤上 alt + enter,就可以提示轉換為 DataBindingLayout了。

image.png

轉換完成就是這樣:

```xml

<data>

</data>

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:viewBindingIgnore="true">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:src="@drawable/splash_center_blue_logo" />

</FrameLayout>

```

可以看到多了一個data的標籤,我們就可以在data中定義變數與變數的型別。

xml <data> <import type="android.util.SparseArray"/> <import type="java.util.Map"/> <import type="java.util.List"/> <import type="android.text.TextUtils"/> <variable name="list" type="List&lt;String&gt;"/> <variable name="sparse" type="SparseArray&lt;String&gt;"/> <variable name="map" type="Map&lt;String, String&gt;"/> <variable name="index" type="int"/> <variable name="key" type="String"/> </data>

import 是定義匯入需要的類,variable是定義需要的變數是由外部傳入,我們可以使用多種方式傳入定義的variable物件。

例如:

```xml

    <variable
        name="viewModel"
        type="com.hongyegroup.cpt_auth.mvvm.vm.UserLoginViewModel" />

    <variable
        name="click"
        type="com.hongyegroup.cpt_auth.ui.UserLoginActivity.ClickProxy" />

    <import type="com.guadou.lib_baselib.utils.NumberUtils" />

</data>

```

使用起來如下: xml <TextView android:id="@+id/tv_get_code" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="@dimen/d_15dp" android:background="@{NumberUtils.isStartWithNumber(viewModel.mCountDownLD)?@drawable/shape_gray_round7:@drawable/shape_white_round7}" android:enabled="@{!NumberUtils.isStartWithNumber(viewModel.mCountDownLD)}" android:paddingLeft="@dimen/d_12dp" android:paddingTop="@dimen/d_5dp" android:paddingRight="@dimen/d_12dp" android:paddingBottom="@dimen/d_5dp" android:text="@={viewModel.mCountDownLD}" android:textColor="@{NumberUtils.isStartWithNumber(viewModel.mCountDownLD)?@color/white:@color/light_blue_text}" android:textSize="@dimen/d_13sp" binding:clicks="@{click.getVerifyCode}" tools:background="@drawable/shape_white_round7" tools:text="Get Code" tools:textColor="@color/light_blue_text" />

頁面的資料都儲存在ViewModel中,頁面的事件都封裝在Click物件中,還能通過NumberUtils直接使用內部的方法了。

在Activity中就可以繫結 Activity 與 DataBinding 了,程式碼如下:

```kotlin class MainActivity : AppCompatActivity() {    private lateinit var mainBinding: ActivityMainBinding    private lateinit var mainViewModel: MainViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)     mainBinding.lifecycleOwner = viewLifecycleOwner

    //設定變數(更容易理解)
    mBinding.setVariable(BR.viewModel,mainViewModel)

    //設定變數(更方便)

mainBinding.viewModel = mainViewModel

} } ```

其中 ActivityMainBinding 這個類就是系統生成的,生成規則是佈局檔名稱轉化為駝峰大小寫形式,然後在末尾新增 Binding 字尾。如 activity_main 編譯為 ActivityMainBinding 。

現在的繫結比剛開始的 DataBinding 真的已經方便很多了。而 Fragment 的繫結有些許不同。

```kotlin class MainFragment : Fragment() {    private lateinit var mainBinding: FragmentMainBinding    private lateinit var mainViewModel: MainViewModel by viewModels()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return setContentView(container) }

fun setContentView(container: ViewGroup?): View {

    mainBinding = DataBindingUtil.inflate<ActivityMainBinding>(layoutInflater, R.layout.fragment_main, container, false)
    mainBinding.lifecycleOwner = viewLifecycleOwner

    //設定變數(更容易理解)
    mBinding.setVariable(BR.viewModel,mainViewModel)

    //設定變數(更方便)

mainBinding.viewModel = mainViewModel

    return mBinding.root
}

} ```

如何在xml使用變數呢?

集合的使用: ```xml

android:text="@{list[index]}"

android:text="@{sparse[index]}"

android:text="@{map[key]}"

```

文字的使用: ```xml android:text="@{user.firstName, default=PLACEHOLDER}"

//常用的三元與判空 android:text="@{user.name != null ? user.name : user.nickName}"

android:text="@{user.name ?? user.nickName}"

android:visibility="@{user.active ? View.VISIBLE : View.GONE}" ```

事件的簡單處理: ```xml android:onClick="@{click::onClickFriend}"

android:onClick="@{() -> click.onSaveClick(task)}"

android:onClick="@{(theView) -> click.onSaveClick(theView, task)}"

android:onLongClick="@{(theView) -> click.onLongClick(theView, task)}"

//控制元件隱藏不設定點選,顯示才設定點選 android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}" ```

雙向繫結:@= 與 @ 的區別 ```xml

<Textview
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{click.etLiveData}" />

```

使用單向繫結的時候@{},viewModel中的資料變化了,就會影響到TextView的顯示。而雙向繫結則是當EditText內部的文字發生變化了也同樣會影響到viewModel中的資料變化。

三、DataBinding的進階使用

關於 DataBinding 的基礎使用,相信大家或多或少都有看過或者用過,知道基礎使用就能在開發中實際開發了嗎?太年輕了!

詳細用過 DataBinding 的或多或少都遇到過一些坑,作為一個常年使用 DataBinding 的開發者,我對下面幾點實際開發中遇到的一些印象深刻的知識點做一些實用的引申。

3.1 RV.Adapter中使用

與 Fragment 的使用方式類似,我們只需要綁定了 View 之後設定給ViewHodler即可。

```kotlin class UserAdapter(users: MutableList, context: Context) :    RecyclerView.Adapter() {

class MyHolder(val binding: TextItemBinding) : RecyclerView.ViewHolder(binding.root) ​    private var users: MutableList = arrayListOf()    private var context: Context ​    init {        this.users = users        this.context = context   } ​    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder {        val inflater = LayoutInflater.from(context)        val binding: TextItemBinding = DataBindingUtil.inflate(inflater, R.layout.text_item, parent, false)        return MyHolder(binding)   } ​    override fun onBindViewHolder(holder: MyHolder, position: Int) {        holder.binding.user = users[position]        holder.binding.executePendingBindings()   }

override fun getItemCount() = users.size } ```

3.2 自定義View的使用

比如我定義一個自定義View,在內部使用了自定義的屬性,需要在 xml 中賦值,

xml <com.guadou.kt_demo.demo.demo12_databinding_texing.CustomTestView android:layout_width="match_parent" android:layout_height="wrap_content" binding:clickProxy="@{click}" binding:testBean="@{testBean}" />

我們再自定義View的類中就可以通過 setXX 拿到這個賦值的屬性了。

```kotlin class CustomTestView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {

init {
    orientation = VERTICAL

    //傳統的方式新增
    val view = CommUtils.inflate(R.layout.layout_custom_databinding_test)
    addView(view)

}

//設定屬性
fun setTestBean(bean: TestBindingBean?) {

    bean?.let {
        findViewById<TextView>(R.id.tv_custom_test1).text = it.text1
        findViewById<TextView>(R.id.tv_custom_test2).text = it.text2
        findViewById<TextView>(R.id.tv_custom_test3).text = it.text3
    }


}

fun setClickProxy(click: Demo12Activity.ClickProxy?) {
    findViewById<TextView>(R.id.tv_custom_test1).click {
        click?.testToast()
    }
}

} ```

如果我們的自定義View不是寫在 XML 中,而是通過Java程式碼手動 add 到佈局中,一樣的可以通過 new 物件,設定自定義屬性來實現一樣的效果:

```kotlin //給靜態的xml,賦值資料,賦值完成之後 include的佈局也可以自動顯示 mBinding.testBean = TestBindingBean("haha2", "heihei2", "huhu2")

//動態的新增自定義View
val customTestView = CustomTestView(mActivity)
customTestView.setClickProxy(clickProxy)
customTestView.setTestBean(TestBindingBean("haha3", "heihei3", "huhu3"))

mBinding.flContent.addView(customTestView)

```

3.3 include與viewStub的使用

include 和 viewStub 的用法差不多,這裡以 include 為例:

例如我們在 Activity 的 xml 佈局中新增一個 include 的佈局。

```xml

<data>
    <variable
        name="testBean"
        type="com.xx.xx.demo.TestBindingBean" /> 
</data>

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:viewBindingIgnore="true">

  ...

     <include
        layout="@layout/include_databinding_test"
        binding:click="@{click}"
        binding:testBean="@{testBean}" />

</FrameLayout>

```

我們可以直接把 Activity 的自定義屬性 testBean 傳入到 include 佈局中。

include_databinding_test: ```xml

<data>

    <variable
        name="testBean"
        type="com.guadou.kt_demo.demo.demo12_databinding_texing.TestBindingBean" />

    <import
        alias="textUtlis"
        type="android.text.TextUtils" />
</data>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:layout_marginTop="15dp"
        android:text="下面是賦值的資料"
        binding:clicks="@{click.testToast}"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@{testBean.text1}" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@{testBean.text2}" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@{testBean.text3}" />

</LinearLayout>

```

這樣在 include 的 xml 中能直接使用自定義屬性來顯示了。

而如果動態的 inflate 佈局就和自定義 View 的處理方式類似了:

```kotlin mBinding.testBean = TestBindingBean("haha", "heihei", "huhu")

//獲取View
val view = CommUtils.inflate(R.layout.include_databinding_test)
//繫結DataBinding 並賦值自定義的資料
DataBindingUtil.bind<IncludeDatabindingTestBinding>(view)?.apply {
    testBean = TestBindingBean("haha1", "heihei1", "huhu1")
}

//添加布局
mBinding.flContent.addView(view)

```

3.4 自定義事件與屬性

重點就是自定義的屬性與事件處理了,一些喜歡在 xml 中寫邏輯的都是基於此方式實現的,下面一起看看如何使用自定義屬性:

Java語言的實現: ```java public class BindingAdapter {

@android.databinding.BindingAdapter("url")
public static void setImageUrl(ImageView imageView, String url) {
    Glide.with(imageView.getContext())
            .load(url)
            .into(imageView);
}

} ```

方法名不是關鍵,關鍵的是註解上面的值 "url",才是在xml中顯示的自定義屬性,而方法中的引數,第一個是限定在哪一個控制元件上生效的,是固定的比傳的引數,而第二個引數 String url 才是我們自定義傳入的引數。

這個例子很簡單,就是傳入url,在 ImageView 上通過 Glide 顯示圖片。

用Kotlin的方法實現就更簡單了:

kotlin @BindingAdapter("url") fun setImageUrl(view: ImageView, url: String?) { if (!url.isNullOrEmpty()) { Glide.with(view.context) .load(imageUrl) .into(view) } }

或者使用Kotlin的頂層擴充套件函式也能實現:

kotlin @BindingAdapter("url") fun ImageView.setImageUrl(url: String?) { if (!url.isNullOrEmpty()) { Glide.with(view.context) .load(imageUrl) .into(this) } }

三種定義的方式都是相同的,除此之外,我們除了加一個引數,我們還能加入多個引數,甚至還能指定可選引數和必填引數:

java @android.databinding.BindingAdapter(value = {"imgUrl", "placeholder"}, requireAll = false) public static void loadImg(ImageView imageView, String url, Drawable placeholder) { GlideApp.with(imageView) .load(url) .placeholder(placeholder) .into(imageView); }

使用: xml <ImageView android:id="@+id/img_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:adjustViewBounds="true" binding:imgUrl="@{user.url}" binding:placeholder="@{@drawable/ic_launcher_background}" />

這裡 requireAll = false 表示我們可以使用這兩個兩個屬性中的任一個或同時使用,如果 requireAll = true 則兩個屬性必須同時使用,不然會在編譯器報錯,現在也 AS 會明確的指出錯誤地方方便修改的。

3.5 自定義轉換器

Converters 轉換器其實是用的比較少,但是在一些特別的場景有奇效,特別是做一些多主題,國際化的時候。

xml <Button android:onClick="toggleIsError" android:text="@{isError ? @color/red : @color/white}" android:layout_width="match_parent" android:layout_height="wrap_content" />

這樣就可以根據顏色來顯示不同的文字:

java @BindingConversion public static int convertColorToString(int color) { switch (color) { case Color.RED: return R.string.red; case Color.WHITE: return R.string.white; } return R.string.black; }

3.6 DataBinding中字串的各種特殊處理

如果說 DataBinding 用的最多的控制元件,那必然是 TextView ,而文字的顯示有多樣的方式,國際化、佔位符、Html/Span等多樣的文字如何在 DataBinding 的 xml 中展示又是一個新的問題。

經過前面的基本使用和部分高階的使用,這裡就直接放程式碼了。

1. databinding使用string format 佔位符:

xml <string name="Generic_Text">My Name is %s</string> android:text= "@{@string/Generic_Text(Profile.name)}"

當然也可以直接使用字串的,但是外面的一層要用單引號

xml android:text='@{viewModel.mHoldAccount,default="22"}'

2. 使用Html標籤

```xml

作品閱讀次數 %1$s 次]]>

... android:text="@{Html.fromHtml(@string/sxx_user_rank(user.readTimes))}" ```

3.Html中使用三元表示式

錯誤方式: xml android:text="@{task.title_total>0?Html.fromHtml(@string/task_title(task.title,task.title_num,task.title_total)):task.title}"

正確方式: xml android:text="@{Html.fromHtml(task.title_total>0?@string/task_title(task.title,task.title_num,task.title_total):task.title)}"

4.default的實現

類似tools的實現: xml android:text="@{viewModel.mYYPayLiveData.reward_points,default=@string/normal_empty}"

等同於: xml android:text="@{viewModel.mYYPayLiveData.reward_points}" tools:text="@string/normal_empty"

類似hilt的實現: xml binding:text="@{viewModel.mSelectBankName}" binding:default="@{@string/normal_empty}" tools:text="@string/normal_empty"

使用自定義屬性完成: kotlin @BindingAdapter("text", "default", requireAll = false) fun setText(view: TextView, text: CharSequence?, default: String?) { if (text == null || text.trim() == "" || text.contains("null")) { view.text = default } else { view.text = text } }

四、DataBinding的簡單原理

ViewBinding的生成過程,就是一系列處理 Tag 的邏輯。將佈局中的含有databinding賦值的 Tag 控制元件存入bindings的Object的陣列中並返回。

image.png

在 ActivityMainBindingImpl 生成類中該方法中將獲取的 View 陣列賦值給成員變數。(相比 findViewById 只遍歷了一次)

DataBinding 通過佈局中的 Tag 將控制元件查找出來,然後根據生成的配置檔案進行對應的同步操作,設定一個全域性的佈局變化監聽來實時更新,通過他的set方法進行同步。

image.png

所以我們才說 DataBinding 不參與檢視邏輯,僅負責通知末端 View 狀態改變,僅用於規避 Null 安全問題。

總的來說,DataBinding 的原理沒有什麼黑科技,就是是基於資料繫結和觀察者模式的。它通過生成程式碼來完成UI元件和資料物件之間的繫結,並使用觀察者模式來保持UI和資料之間的同步。

五、簡化DataBinding的使用(封裝)

可能有同學看了基本的使用和一些進階的使用之後,更堅定了心中的想法,可去你的吧,使用這麼麻煩,狗都不用...😅😅

別急,我們還能對一些固定的場景化的用法做一些封裝嘛,反正常用的幾種方法,有限並不包括於一些字串處理,圖片處理,資料介面卡的處理,UI的處理等一些方法定義好了或者封裝好了使用起來就是so easy!

5.1 Activity/Fragment主頁面封裝

一般關於Activity/Fragment 我們主要是封裝的 DataBinding 與 ViewModel。

不同的人有不同的封裝方法,有的用泛型+傳參的方式,有的用泛型+反射的方式,有的封裝了 DataBinding 的填充自定義屬性邏輯。

下面分別演示不同的封裝方式:

```kotlin abstract class BaseVDBActivity( private val vmClass: Class, private val vb: (LayoutInflater) -> VB, ) : AppCompatActivity() {

//由於傳入了引數,可以直接構建ViewModel
protected val mViewModel: VM by lazy {
    ViewModelProvider(viewModelStore, defaultViewModelProviderFactory).get(vmClass)
}

//如果使用DataBinding,自己再賦值

}

```

這種方法使用了泛型+傳參,使用的時候需要填入構造引數:

kotlin class MainActivity : BaseVDBActivity<ActivityMainBinding, MainViewModel>( ActivityMainBinding::inflate, MainViewModel::class.java ) { //就可以直接使用ViewBinding與ViewModel fun test() { mBinding.iconIv.visibility = View.VISIBLE mViewModel.data1.observe(this) { } } }

如果是使用的 DataBinding,我們還能把 DataBinding 的屬性賦值邏輯進行封裝:

封裝一個Config物件 ```kotlin class DataBindingConfig( private val layout: Int, private val vmVariableId: Int, private val stateViewModel: BaseViewModel ) {

private var bindingParams: SparseArray<Any> = SparseArray()

fun getLayout(): Int = layout

fun getVmVariableId(): Int = vmVariableId

fun getStateViewModel(): BaseViewModel = stateViewModel

fun getBindingParams(): SparseArray<Any> = bindingParams

fun addBindingParams(variableId: Int, objezt: Any): DataBindingConfig {
    if (bindingParams.get(variableId) == null) {
        bindingParams.put(variableId, objezt)
    }
    return this
}

} ```

使用 Config 物件給 DataBinding 賦值自定義屬性的封裝:

```kotlin abstract class BaseVDBActivity : BaseVMActivity() {

protected lateinit var mBinding: VDB

protected abstract fun getDataBindingConfig(): DataBindingConfig

override fun getLayoutRes(): Int = -1

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    mBinding = DataBindingUtil.setContentView(this, getDataBindingConfig().getLayout())
    mBinding.lifecycleOwner = this
    mBinding.setVariable(
        getDataBindingConfig().getVmVariableId(),
        getDataBindingConfig().getStateViewModel()
    )
    val bindingParams = getDataBindingConfig().getBindingParams()
    bindingParams.forEach { key, value ->
        mBinding.setVariable(key, value)
    }
    init(savedInstanceState)
}

} ```

Fragment的封裝也是大同小異:

```kotlin abstract class BaseVDBFragment : BaseVMFragment() {

protected lateinit var mBinding: VDB

override fun getLayoutRes(): Int = -1

protected abstract fun getDataBindingConfig(): DataBindingConfig

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    mBinding =
        DataBindingUtil.inflate(inflater, getDataBindingConfig().getLayout(), container, false)
    mBinding.lifecycleOwner = viewLifecycleOwner
    mBinding.setVariable(
        getDataBindingConfig().getVmVariableId(),
        getDataBindingConfig().getStateViewModel()
    )
    val bindingParams = getDataBindingConfig().getBindingParams()
    bindingParams.forEach { key, value ->
        mBinding.setVariable(key, value)
    }
    return mBinding.root
}

} ```

我們使用的時候就直接賦值自定義屬性:

```kotlin class ProfileFragment : BaseFragment() {

override fun getDataBindingConfig(): DataBindingConfig {
    return DataBindingConfig(R.layout.fragment_profile, BR.viewModel, mViewModel)
        .addBindingParams(BR.click, ClickProxy())
}

private val articleAdapter by lazy { ArticleAdapter(requireContext()) }

...

} ```

具體的程式碼太多了,可以參照文章結尾的專案。

5.2 RV.Adapter的封裝

其實在之前的 RV.Adapter 使用中,我們也能基於這個 Adapter 封裝,但是我們專案中使用的還是BRVAH,所以我們就基於此封裝的。

```kotlin open class BaseBindAdapter(layoutResId: Int, br: Int) : BaseQuickAdapter(layoutResId) {

private val _br: Int = br

override fun convert(helper: BindViewHolder, item: T) {
    helper.binding.run {
        setVariable(_br, item)
        executePendingBindings()
    }
}

override fun getItemView(layoutResId: Int, parent: ViewGroup?): View {
    val binding = DataBindingUtil.inflate<ViewDataBinding>(mLayoutInflater, layoutResId, parent, false)
            ?: return super.getItemView(layoutResId, parent)
    return binding.root.apply {
        setTag(R.id.BaseQuickAdapter_databinding_support, binding)
    }
}

class BindViewHolder(view: View) : BaseViewHolder(view) {
    val binding: ViewDataBinding
        get() = itemView.getTag(R.id.BaseQuickAdapter_databinding_support) as ViewDataBinding
}

} ```

使用的時候,可以選擇繼承這個基類實現:

```kotlin class HomeArticleAdapter(layoutResId: Int = R.layout.item_article_constraint) : BaseBindAdapter

(layoutResId, BR.article) {

override fun convert(helper: BindViewHolder, item: Article) {
    super.convert(helper, item)

    helper.addOnClickListener(R.id.articleStar)
    helper.setImageResource(R.id.articleStar, if (item.collect) R.drawable.timeline_like_pressed else R.drawable.timeline_like_normal)
    else helper.setVisible(R.id.articleStar, false)

    helper.setText(R.id.articleAuthor,if (item.author.isBlank()) "分享者: ${item.shareUser}" else item.author)
    Timer.stop(APP_START)
}

} ```

甚至在一些簡單的佈局展示邏輯,我們都無需繼承基類實現,直接:

xml private val systemAdapter by lazy { BaseBindAdapter<SystemParent>(R.layout.item_system, BR.systemParent) }

5.3 常用的自定義屬性與事件效果

EditText:

```kotlin /* * EditText的簡單監聽事件 / @BindingAdapter("onTextChanged") fun EditText.onTextChanged(action: (String) -> Unit) { addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable?) { }

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        action(s.toString())
    }
})

}

var _viewClickFlag = false var _clickRunnable = Runnable { _viewClickFlag = false }

/* * Edit的確認按鍵事件 / @BindingAdapter("onKeyEnter") fun EditText.onKeyEnter(action: () -> Unit) { setOnKeyListener { _, keyCode, _ -> if (keyCode == KeyEvent.KEYCODE_ENTER) { KeyboardUtils.closeSoftKeyboard(this)

        if (!_viewClickFlag) {
            _viewClickFlag = true
            action()
        }
        removeCallbacks(_clickRunnable)
        postDelayed(_clickRunnable, 1000)
    }
    return@setOnKeyListener false
}

}

/* * Edit的失去焦點監聽 / @BindingAdapter("onFocusLose") fun EditText.onFocusLose(action: (textView: TextView) -> Unit) { setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) { action(this) } } }

/* * 設定ET小數點2位 / @BindingAdapter("setDecimalPoints") fun setDecimalPoints(editText: EditText, num: Int) { editText.filters = arrayOf(ETMoneyValueFilter(num)) } ```

ImageView:

```kotlin /* * 設定圖片的載入 / @BindingAdapter("imgUrl", "placeholder", "isOriginal", "roundRadius", "isCircle", requireAll = false) fun loadImg( view: ImageView, url: Any?, placeholder: Drawable? = null, isOriginal: Boolean = false, roundRadius: Int = 0, isCircle: Boolean = false ) { url?.let { view.extLoad( it, placeholder = placeholder, roundRadius = CommUtils.dip2px(roundRadius), isCircle = isCircle, isForceOriginalSize = isOriginal ) } }

@BindingAdapter("loadBitmap") fun loadBitmap(view: ImageView, bitmap: Bitmap?) { view.setImageBitmap(bitmap) } ```

TextView:

```kotlin //為空的時候設定預設值 @BindingAdapter("text", "default", requireAll = false) fun setText(view: TextView, text: CharSequence?, default: String?) { if (text == null || text.trim() == "" || text.contains("null")) { view.text = default } else { view.text = text } }

//設定Html字型 @BindingAdapter("textHtml") fun setTextHtml(textView: TextView, text: String?) { if (!TextUtils.isEmpty(text)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { textView.text = Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY) } else { textView.text = Html.fromHtml(text) } } else { textView.text = "" } }

/* * 設定左右的Drawable圖示 / @BindingAdapter("setRightDrawable") fun setRightDrawable(textView: TextView, drawable: Drawable?) { if (drawable == null) { textView.setCompoundDrawables(null, null, null, null) } else { drawable.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight) textView.setCompoundDrawables(null, null, drawable, null) } }

@BindingAdapter("setLeftDrawable") fun setLeftDrawable(textView: TextView, drawable: Drawable?) { if (drawable == null) { textView.setCompoundDrawables(null, null, null, null) } else { drawable.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight) textView.setCompoundDrawables(drawable, null, null, null) } }

```

View:

```kotlin /* * 設定控制元件的隱藏與顯示 / @BindingAdapter("isVisibleGone") fun isVisibleGone(view: View, isVisible: Boolean) { view.visibility = if (isVisible) View.VISIBLE else View.GONE }

@BindingAdapter("isInVisibleShow") fun isInVisible(view: View, isVisible: Boolean) { view.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE }

/* * 點選事件防抖動的點選 / @BindingAdapter("clicks") fun clicks(view: View, action: () -> Unit) { view.click { action() } }

/* * 重新設定高度 / @BindingAdapter("layoutHeight") fun layoutHeight(view: View, targetHeight: Float) { val height = view.layoutParams.height

if (height != targetHeight.toInt()) {
    view.apply {
        this.layoutParams = layoutParams.apply {
            this.height = targetHeight.toInt()
        }
    }
}

}

//設定動畫設定高度 @SuppressLint("Recycle") @BindingAdapter("layoutHeightAnim") fun layoutHeightAnim(view: View, targetHeight: Float) { val layoutParams = view.layoutParams val height = layoutParams.height

if (height != targetHeight.toInt()) {

    //值的屬性動畫
    val animator = ValueAnimator.ofInt(height, targetHeight.toInt()).apply {

        addUpdateListener {
            val heightVal = it.animatedValue as Int
            layoutParams.height = heightVal
            view.layoutParams = layoutParams
        }

        duration = 250
    }

    //不能再子執行緒中更新UI,如果是其他的值是可以的比如Tag
    AsyncAnimUtil.instance.startAnim(view.findViewTreeLifecycleOwner(), animator, false)
}

}

```

由於篇幅原因只貼出了自用的相對重要的部分,如果想要檢視完整的可以去文章末尾檢視原始碼展示。

總結

DataBinding 對比 findviewbyid 對比的優缺點:

優點: 1. 簡化 findviewbyid 模板程式碼,更簡潔易懂。 2. 支援雙向繫結與單向繫結,可選可配置,更靈活。 3. xml佈局與頁面的一一對應,儘量減少空指標異常,配合 Kotlin 的非空校驗更舒適。 4. 通過生成的繫結類減少程式碼執行時間,內部還註冊物件的懶載入,可以帶來一定的效能優化。 5. 方便做換膚與國際化,可以通過介面卡更精細的操作樣式與文字。

缺點: 1. 相容性問題(升級AS版本與Gradle版本) 2. 不方便除錯(再次推薦不要在XML裡寫邏輯,並且目前AS升級後已經能明確指出大部分的問題) 3. 編譯時間更長了(特別是第一次需要生成很多的Bind類檔案,再次執行有快取和增量更新會好一點) 4. 少量增加APK體積(畢竟多了很多類)

使用DataBinding的一些小Tips:

1.想用雙向繫結就用,不想用雙向繫結就用單向繫結,都不想用只用findviewbyid也是可以的。完全看大家的喜歡,當然不用DataBinding/ViewBinding 也行的,可以用其他的框架或者原生的findviewbyid都行的。

2.如果要啟動 DataBinding ,推薦你順便加上 ViewBinding buildFeatures { viewBinding = true dataBinding = true }

DataBinding是 ViewBinding 的超集,如果只想替換findviewbyid的功能,那你可以使用使用 ViewBinding ,如果想強制指定不生成 ViewBinding 編譯檔案,可以加上tools:viewBindingIgnore="true"

3.DataBinding雖然支援可以在xml裡面寫複雜的計算邏輯,但還是推薦大家儘量只做資料的繫結,邏輯計算儘量不要解除安裝xml裡面,如果真要寫邏輯,最多隻做三元的邏輯判斷。以免出現一些效能問題與除錯問題。

4.DataBinding配合ViewModel和LiveData食用更舒適,可以繫結生命週期也推薦大家要繫結到lifecycleOwner,它可以自動銷燬資源,在此場景中 Flow 反而沒有 LiveData 好用,並且在部分版本中 LiveData 反而相容性更好。

5.xml 的標籤儘量把自定義屬性的 app 標籤與 DataBinding 標籤 databinding 區分開來便於後期的維護和同事的協同開發。

6.善用 BindingAdapter 進行資料繫結與設定監聽。

總的來說用還是不用 DataBinding 還真是存乎一心,都行,只是我個人覺得在當下這個時間點看的話是利大於弊。再往後我也不好說,畢竟 Compose 把整個 xml 體系都給革命了。

說到這裡請容許我掙扎一下先給自己疊個甲:

我認為原生 Android 的未來一定是 Compose ,但是多少年之後能走向主流不好說,3年?5年?畢竟 Kotlin 語言推出到今年這麼多年了也只和 Java 55開而已,甚至我認識的好多5年以上的開發者都沒用過 Kotlin,反而目前主流的 MVVM 中還是很多是使用 DataBinding 的,就算我們不用也是需要了解的。

可能真的有很多人對 DataBinding 不喜歡、不感冒,也能理解。其實我也是各種機緣巧合下才入的坑,我也是從開始的嫌棄,到真香,再放棄,最後一直使用至今。

沒有最好的框架,只有最合適的框架。

結局慣例,我如有講解不到位或錯漏的地方,希望同學們可以指出。如果有更好的使用方式或封裝方式,或者你有遇到的坑也都可以在評論區交流一下,互相學習進步。

如果感覺本文對你有一點點的幫助,還望你能點贊支援一下,你的支援是我最大的動力。

本文的部分程式碼可以在我的 Kotlin 測試專案中看到,【傳送門】。你也可以關注我的這個Kotlin專案,我有時間都會持續更新。

關於 MVVM 架構 和 DataBinding 框架與其他 Jetpack 的實戰專案,如果大家有興趣可以看看大佬的專案 難得一見 Jetpack MVVM 最佳實踐

Ok,這一期就此完結。

本文正在參加「金石計劃」

「其他文章」