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,這一期就此完結。

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

「其他文章」