Kotlin 實戰 | 幹掉 findViewById 和 Activity 中的業務邏輯

上一篇介紹了運用 Kotlin DSL 構建佈局的方法,相較於 XML,可讀性和性能都有顯著提高。若是這套 DSL 還能數據綁定就更好了,這樣就能去掉 findViewById 和 Activity 中的業務邏輯代碼(findViewById 須要遍歷 View 樹,這是耗時的,而 Activity 中的業務邏輯讓其變得愈加臃腫)。這一篇就介紹一種實現思路。android

這是該系列的第十篇,系列文章目錄以下:git

  1. Kotlin基礎 | 白話文轉文言文般的Kotlin常識github

  2. Kotlin基礎 | 望文生義的Kotlin集合操做性能優化

  3. Kotlin實戰 | 用實戰代碼更深刻地理解預約義擴展函數bash

  4. Kotlin實戰 | 使用DSL構建結構化API去掉冗餘的接口方法服務器

  5. Kotlin基礎 | 屬性也能夠是抽象的架構

  6. Kotlin進階 | 動畫代碼太醜,用DSL動畫庫拯救,像說話同樣寫代碼喲!app

  7. Kotlin基礎 | 用約定簡化相親dom

  8. Kotlin基礎 | 2 = 12 ?泛型、類委託、重載運算符綜合應用ide

  9. Kotlin實戰 | 語法糖,總有一顆甜到你(持續更新)

  10. Kotlin 實戰 | 幹掉 findViewById 和 Activity 中的業務邏輯

數據綁定原始方式

在沒有 Data Binding 以前,咱們是這樣爲控件綁定數據的:

class MainActivity : AppCompatActivity() {
    private var tvName: TextView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //'獲取控件引用'
        tvName = findViewById<TextView>(R.id.tvName)
    }
    
    override fun onUserReturn(user: User){
        //'在數據返回時設置控件'
        tvName?.text  = user.name
    }
}
複製代碼

tvName被靜態地聲明在 XML 中,程序動態地經過findViewById()獲取引用,在數據返回的地方調用設值 API。

靜態的意味着能夠預先定義,且保持不變。而動態的偏偏相反。

對於某個特定的業務場景,除了界面佈局是靜態的以外,佈局中某個控件和哪一個數據綁定也是靜態的。這種綁定關係最初是經過「動態」代碼實現的,直到出現了Data Binding

Data Binding

它是 Google 推出的一種將數據和控件相關聯的方法。

若是把 XML 稱爲聲明型的,那 Kotlin 代碼就是程序型的,前者是靜態的,後者是動態的。爲了讓它倆關聯,Data Binding 的思路是把程序型的變量引入到聲明型的佈局中,好比下面把 User.name 綁定到 TextView 上( data_binding_activity.xml ):

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.test.User"/>
   </data>

    <TextView 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@{user.name}"/>
</layout>
複製代碼

其中User是程序型的實體類:

package com.test

data class User(var name: ObservableField<String>, var age: ObservableField<Int>)
複製代碼

ObservableField用於將任何類型包裝成可被觀察的對象,當對象值發生變化時,觀察者就會被通知。在 Data Binding 中,控件是觀察者。

在 Activity 中,這樣寫代碼就完成了數據綁定:

class MainActivity: AppCompatActivity() {
    //'聲明在 Activity 中的數據源'
    private var user:User? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //'爲 Activity 設置佈局並綁定控件'
        val binding = DataBindingUtil.setContentView<DataBindingActivityBinding>(this, R.layout.data_binding_activity);
        //'綁定數據源'
        binding.user = this.user
    }

    override fun onUserReturn(user: User){
        //'修改數據源'
        this.user.name.set( user.name )
    }
}
複製代碼

這樣寫的好處是,Activity 中不會再出現findViewById()和各類爲控件設置屬性的方法,而只須要觀察數據源的變更。

爲 DSL 添加數據綁定

回顧下上一篇構建佈局的 DSL :

class MainActivity : AppCompatActivity() {

    //'構建佈局'
    private val rootView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_id = "tvName"
                layout_width = wrap_content
                layout_height = wrap_content
                textSize = 30f
                textStyle = bold
                align_vertical_to = parent_id
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //'將佈局設置爲 Activity 的 content view'
        setContentView(rootView)
    }
}
複製代碼

Activity content view 的根佈局是 ConstraintLayout,其中包含一個 TextView。爲了讓它的值和User.name聯動,新增擴展屬性以下:

inline var TextView.bindText: LiveData<CharSequence>?
    get() {
        return null
    }
    set(value) {
        //'爲 TextView 的 text 屬性綁定數據源'
        observe(value) { text = it }
    }

//'爲控件綁定 LiveData 類型的數據源'
fun <T> View.observe(liveData: LiveData<T>?, action: (T) -> Unit) {
    (context as? LifecycleOwner)?.let { owner ->
        liveData?.observe(owner, Observer { action(it) })
    }
}
複製代碼

爲 View 新增一個擴展方法,用於在 View 生命週期內觀察數據源 LiveData 的變化。當數據源發生變化時執行action

而後就能夠像這樣爲 TextView 綁定數據源了:

class MainActivity : AppCompatActivity() {

    private val nameLiveData = MutableLiveData<CharSequence>()

    private val rootView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_id = "tvName"
                layout_width = wrap_content
                layout_height = wrap_content
                textSize = 30f
                textStyle = bold
                //'綁定數據源'
                bindText = nameLiveData
                align_vertical_to = parent_id
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(rootView)
    }
    
    override fun onUserReturn(user: User){
        //'數據源變動'
        nameLiveData.value = user.name
    }
}
複製代碼

佈局 DSL 和界面類定義在同一個kt文件中,因此它能方便地訪問到各類數據源。

把數據源都抽象爲LiveData<T>, 控件中每個須要綁定數據的屬性,均可覺得其擴展一個bindXXX屬性,它的值是LiveData<T>

上面的例子雖然運用了數據綁定LiveData,但仍是沿用了MVP的架構。在業務邏輯更復雜的場景,MVP架構下的 Activity 類就會變得愈來愈臃腫。

再來看一個業務邏輯更復雜的例子。不一樣性別的用戶名稱有不一樣顏色:

class MainActivity : AppCompatActivity() {

    //'應該讓 ViewModel 持有 LiveData'
    private val nameLiveData = MutableLiveData<CharSequence>()

    //'構建佈局應該在 Activity 層完成'
    private val rootView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_id = "tvName"
                layout_width = wrap_content
                layout_height = wrap_content
                textSize = 30f
                textStyle = bold
                //'數據源和控件的綁定應該在Activity層完成'
                bindText = nameLiveData
                align_vertical_to = parent_id
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(rootView)
    }
    
    override fun showUserName(user: User){
        //'數據源變動(這段業務邏輯應該寫在 ViewModel 裏面)'
        nameLiveData.value = SpannableStringBuilder(user.name).apply {
            setSpan(ForegroundColorSpan(Color.RED), 0, it.name.indexOf(" "), Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
            val color = if (user.gender == 1) "#b300ff00" else "#b3ff00ff"
            setSpan(ForegroundColorSpan(Color.parseColor(color)), user.name.indexOf(" "), user.name.lastIndex + 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
        }
    }
}
複製代碼

這裏的業務邏輯是:用戶的姓和名之間會以空格分隔,將用戶的姓展現爲紅色,而名隨性別的變化而變色。

demo 是按照 MVP 來寫的,但沒有展現全部架構的細節,用文字補充以下:

  1. Activity 經過 Presenter 請求用戶數據。
  2. Presenter 將服務器返回的 Json 轉換成 User 類,並調用 View 層接口showUserName()通知界面刷新(由 Activity 實現)。
  3. Activity 在 View 層接口中拿到數據並調用控件設值 API 展現數據。

如有了數據綁定,則能夠把 Presenter 和 Activity 間通訊都去掉,這也正是 MVP 模式被詬病的地方,即 View 層接口會隨着業務邏輯而膨脹。

若改用 MVVM 架構從新實現一邊 demo,應該是這樣的:

  1. LiveData 實例應該被 ViewModel 持有。
  2. 「數據源變動」這種業務邏輯都應該寫到 ViewModel 中。
  3. ViewModel 經過 LiveData 將數據源通知出去,而 LiveData 和 控件在 Activity 層完成了綁定。

這樣 Activity 裏面就再也不有 findViewById 和 業務邏輯代碼。

想要擴展這套 「DSL + 數據綁定」 方案也極爲方便,好比爲 ImageView 添加一個綁定 url 的屬性:

inline var ImageView.bindSrc: LiveData<Bitmap>?
    get() {
        return null
    }
    set(value) {
        observe(value) { setImageBitmap(it) }
    }
複製代碼

先爲 ImageView 控件擴展一個名爲bindSrc的屬性,它是LiveData<Bitmap>?類型的。

而後在 構建佈局 DSL 中就能夠這樣使用該屬性(爲簡單起見仍是用 MVP):

class FirstFragment : Fragment() {
    //'數據源'
    val avatarLiveData = MutableLiveData<Bitmap>()

    //'數據源發生變動'
    private val target = object : SimpleTarget<Bitmap>() {
        override fun onResourceReady(resource: Bitmap?, glideAnimation: GlideAnimation<in Bitmap>?) {
            avatarLiveData.value = resource
        }
    }
    //構建佈局
    private val rootView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            ImageView {
                layout_width = 40
                layout_height = 40
                //'和數據源綁定'
                bindSrc = avatarLiveData
            }
        }
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return rootView
    }
    
    override fun showUser(user: User){
        //'觸發加載數據源'
        Glide.with(context).load(user.url).asBitmap().into(target)
    }
}
複製代碼

相較於 DataBinding 中自定義 BindingAdapter 更簡單一丟丟。

talk is cheap, show me the code

推薦閱讀

  1. Kotlin基礎:白話文轉文言文般的Kotlin常識
  2. Kotlin實戰:使用DSL構建結構化API去掉冗餘的接口方法
  3. Kotlin進階:動畫代碼太醜,用DSL動畫庫拯救,像說話同樣寫代碼喲!
  4. Android性能優化 | 把構建佈局用時縮短 20 倍(上)
  5. Android性能優化 | 把構建佈局用時縮短 20 倍(下)
相關文章
相關標籤/搜索