Kotlin實戰:用實戰代碼更深刻地理解預約義擴展函數

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

  1. Kotlin基礎:白話文轉文言文般的Kotlin常識編程

  2. Kotlin基礎:望文生義的Kotlin集合操做安全

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

  4. Kotlin實戰:使用DSL構建結構化API去掉冗餘的接口方法app

  5. Kotlin基礎:屬性也能夠是抽象的ide

  6. Kotlin進階:動畫代碼太醜,用DSL動畫庫拯救,像說話同樣寫代碼喲!函數式編程

  7. Kotlin基礎:用約定簡化相親函數

理論和實踐之間那條鴻溝一直在那,若是不去跨越,就只能發出「懂了這麼多道理,依然過很差這一輩子」這樣的感嘆。這一篇就試着用項目中的實戰代碼來跨越這條鴻溝。本文的口號是「demo code is cheap, show me the real project code!」。包含以下知識點:函數類型、擴展函數、帶接收者的lambda、apply()、also()、let()、安全調用運算符、Elvis運算符。post

apply()

第一篇中提到過apply()函數,這一次結合實戰代碼,講的更深刻一點。動畫

在 Android 將多個動畫組合在一塊兒會用到 AnimatorSet,使用apply()可讓構建組合動畫的代碼減小重複的對象名,讓整個構建語義一目瞭然:

val span = 300
AnimatorSet().apply {
    playTogether(
            ObjectAnimator.ofPropertyValuesHolder(
                    tvTitle,
                    PropertyValuesHolder.ofFloat("alpha", 0f, 1.0f),
                    PropertyValuesHolder.ofFloat("translationY", 0f, 100f)).apply {
                interpolator = AccelerateInterpolator()
                duration = span
            },
            ObjectAnimator.ofPropertyValuesHolder(
                    ivAvatar,
                    PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f),
                    PropertyValuesHolder.ofFloat("translationY", 0f,100f)).apply {
                interpolator = AccelerateInterpolator()
                duration = span
            }
    )
    start()
}
複製代碼

同時對 tvTitle 和 ivAvatar 控件作透明度和位移動畫,並設置了動畫時間和插值器。代碼中沒有出現多個AnimatorSet對象及多個Animator對象,這都得益於apply()

  1. object.apply()接收一個 lambda 做爲參數。它的語義是:將lambda應用於object對象,其中的 lambda 是一種特殊的 lambda,稱爲帶接收者的lambda。這是 kotlin 中特有的,java 中沒有。

    帶接收者的lambda的函數體除了能訪問其所在類的成員外,還能訪問接收者的全部非私有成員,這個特性是它具備魅力的關鍵。(這個特性還使得它很是適用於構建DSL,下一篇會提到)

    上述代碼中緊跟在apply()後的 lambda 函數體除了訪問其外部的變量span,還訪問了 AnimatorSet 的playTogether()start(),就好像在 AnimatorSet 類內部同樣。(能夠在這兩個函數前面加上this,省略了更簡潔)。

  2. object.apply()的另外一個特色是:在它對 object 對象進行了一段操做後還會返回 object 對象自己。

因此apply()適用於 「構建對象後緊接着還須要調用該對象的若干方法進行設置並最終返回這個對象實例」 的場景

let()

let()apply()很是像,但由於下面的兩個區別,使得它的應用場景和apply()不太同樣:

  1. 它接收一個普通的 lambda 做爲參數。
  2. 它將 lambda 的值做爲返回值。

在項目中有這樣一個場景:啓動一個 Fragment 並傳 bundle 類型的參數,若是其中的 duration 值不爲 0 則顯示視圖A,不然顯示視圖B。用let()實現以下:

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        arguments?.let { arg ->
            //調用對象方法
            arg.getBundle(KEY)?.takeIf { it[DURATION] != 0 }?.let { duration -> 
                //將對象做爲參數傳遞給另外一個函數
                showA(duration)
            } ?: showB()
        }
    }
}
複製代碼

上述代碼展現了let()的三個用法慣例:

  1. 一般狀況下let()會和安全調用運算符?一塊兒使用,即object?.let(),它的語義是:若是object不爲空則對它作一些操做,這些操做能夠是調用它的方法,或者將它做爲參數傳遞給另外一個函數

    apply()對比一下,由於apply()一般用於構建新對象(let()用於既有對象),新建的對象不可能爲空,因此不須要?,並且就使用習慣而言,apply()後的 lambda 中一般只有調用對象的方法,而不會將對象做爲參數傳遞給另外一個函數(雖然也能夠這麼作,只要傳this就能夠)

  2. let()也會結合Elvis運算符?:實現空值處理,當調用let()的對象爲空時,其 lambda 中的邏輯不會被執行,若是須要指定此時執行的邏輯,可使用?:

  3. let()嵌套時,顯示地指明 lambda 參數名稱避免it的歧義。在 kotlin 中若是 lambda 參數只有一個 能夠將參數聲明省略,並用it指代它。但當 lambda 嵌套時,it的指向就有歧義。因此代碼中用arg顯示指明這是 Fragment 的參數,用duration顯示指明這是 Bundle 中的 duration。

除了上面這種用法,還能夠把let()當作變換函數使用,就好像RxJava中的map()操做符。由於let()將 lambda 的值做爲其返回值。

在項目中有這樣一個需求:爲某界面的全部點擊事件添加數據埋點。

固然能夠將埋點邏輯散落在各個控件的OnClickListener中,但若是但願對埋點邏輯統一控制,就能夠用下面的這個方案:

  1. 先定義一個包含點擊響應邏輯的類
class OnClickListenerBuilder {
    //'點擊響應邏輯'
    var onClickAction: ((View) -> Unit)? = null

    //'爲點擊響應邏輯賦值的函數'
    fun onClick(action: (View) -> Unit) {
        onClickAction = action
    }
}
複製代碼

函數式編程中,把函數當作值來對待,你能夠把函數當作值處處傳遞,也能夠把函數獨立地聲明並存儲在一個變量中,可是最多見的仍是直接聲明它並傳遞給函數做爲參數。

OnClickListenerBuilder定義了一個函數類型的成員變量onClickAction,它的類型是((View) -> Unit)?,這和View.OnClickListener中的void onClick(View view)函數一摸同樣,即輸入一個View並返回空值。這個成員變量的值是可空的,因此在本來的函數類型(View) -> Unit外面又套了一個括號和問號。

這個類的目的是將自定義的點擊響應邏輯保存在函數類型的變量中,當點擊事件發生時應用這段邏輯。

  1. 爲 View 設置點擊事件並應用自定義點擊響應邏輯
//'定義擴展函數'
fun View.setOnDataClickListener(action: OnClickListenerBuilder.() -> Unit) {
    setOnClickListener(
            OnClickListenerBuilder().apply(action).let { builder ->
                View.OnClickListener { view ->
                    //'埋點邏輯'
                    Log.v(「ttaylor」, 「view{$view} is clicked」)
                    //'點擊響應邏輯'
                    builder.onClickAction?.invoke(view)
                }
            }
    )
}

//'在界面中使用擴展函數爲控件設置點擊事件'
btn.setOnDataClickListener {
    onClick {
        Toast.makeText(this@KotlinExample, 「btn is click」, Toast.LENGTH_LONG).show()
    }
}
複製代碼
  • 擴展函數

    View聲明瞭一個擴展函數setOnDataClickListener()。擴展函數是一個類的成員函數,但它定義在類體外面。這樣定義的好處是,能夠在任什麼時候候任何地方給類添加功能。

    在擴展函數中,能夠像類的其餘成員函數同樣訪問類的屬性和方法(除了被private和protected修飾的成員),在本例中調用了setOnClickListener()爲控件設置點擊事件。

  • 帶接收者的lambda

    爲了應用保存在OnClickListenerBuilder中的點擊響應邏輯,必須先構建其實例。代碼中的OnClickListenerBuilder().apply(action)在構建實例的同時對其應用了一個 lambda ,這是一個帶接收者的lambda,且接收者是OnClickListenerBuilder。它做爲擴展函數的參數傳入。當調用setOnDataClickListener()的時候,咱們在傳入的 lambda 中輕鬆地調用了onClick()方法,由於帶接收者的lambda函數體中能夠訪問接收者的非私有成員。這樣就實現了將點擊響應邏輯保存在函數類型變量onClickAction中。

  • let()map()

    因爲 API 的限定View.setOnClickListener()的參數必須是View.OnClickListener類型的,因此必須將OnClickListenerBuilder轉化成View.OnClickListener。調用let()就能夠輕鬆作到,由於它將 lambda 的值做爲返回值。

    最後在原生點擊事件響應函數中實現了埋點邏輯並調用了保存在函數類型變量中的自定義點擊響應邏輯。

also()

also()幾乎和let()相同,惟一的卻別是它會返回調用者自己而不是將 lambda 的值做爲返回值。

和一樣返回調用者自己的apply()相比:

  1. 就傳參而言,apply()傳入的是帶接收者的lambda,而also()傳入的是普通 lambda。因此在 lambda 函數體中前者經過this引用調用者,後者經過it引用調用者(若是不定義參數名字,默認爲it)
  2. 就使用場景而言,apply()更多用於構建新對象並執行一頓操做,而also()更多用於對既有對象追加一頓操做。

在項目中,有一個界面初始化的時候須要加載一系列圖片並保存到一個列表中:

listOf(
    R.drawable.img1,
    R.drawable.img2, 
    R.drawable.img3,
    R.drawable.img4, 
).forEach {resId->
    BitmapFactory.decodeResource(
        resources, 
        resId, 
        BitmapFactory.Options().apply {
            inPreferredConfig = Bitmap.Config.ARGB_4444
            inMutable = true
        }
    ).also { bitmap -> imgList.add(bitmap) }
}
複製代碼

這個場景中用let()也沒什麼不能夠。可是若是還須要將解析的圖片輪番顯示出來,用also()就再好不過了:

listOf(
    R.drawable.img1,
    R.drawable.img2, 
    R.drawable.img3,
    R.drawable.img4, 
).forEach {resId->
    BitmapFactory.decodeResource(
        resources, 
        resId, 
        BitmapFactory.Options().apply {
            inPreferredConfig = Bitmap.Config.ARGB_4444
            inMutable = true
        }
    ).also { 
        //存儲邏輯
        bitmap -> imgList.add(bitmap) 
    }.also {
        //顯示邏輯
        ivImg.setImageResource(it)   
    }
}
複製代碼

由於also()返回的是調用者自己,因此能夠also()將不一樣類型的邏輯分段,這樣的代碼更容易理解和修改。這個例子邏輯比較簡單,只有一句話,將他們合併在一塊兒也沒什麼很差。

知識點總結

  • 在函數式編程中,把函數當作值來對待,你能夠把函數當作值處處傳遞,也能夠把函數獨立地聲明並存儲在一個變量中。
  • 函數類型是一種新的類型,它用 lambda 來描述一個函數的輸入和輸出。
  • 擴展函數是一種能夠在類體外爲類新增功能的特性,在擴展函數體中能夠訪問類的成員(除了被private和protected修飾的成員)
  • 帶接收者的lambda是一種特殊的lambda,在函數體中能夠訪問接收者的非私有成員。能夠把它理解成接收者的擴展函數,只不過這個擴展函數沒有函數名。
  • apply() also() let()是系統預約義的擴展函數。用於簡化代碼,減小重複的對象名。
  • ?.稱爲安全調用運算符,若object?.fun()中的 object 爲空,則fun()不會被調用。
  • ?:稱爲Elvis運算符,它爲 null 提供了默認邏輯,funA() ?: funB(),若是 funA() 返回值不爲 null 執行它 並將它的返回值做爲整個表達式的返回值,不然執行 funB() 並採用它的返回值。

下一篇會在這篇的基礎上講解一個更加複雜的例子並引出DSL的概念。

相關文章
相關標籤/搜索