當 Kotlin 中的監聽器包含多個方法時,如何讓它 「巧奪天工」?

當 Kotlin 中的監聽器包含多個方法時,如何讓它 「巧奪天工」?

我常常遇到的一個問題是在使用 Kotlin 時如何簡化具備多個方法的監聽器的交互。對於具備只具備一個方法的監聽器(或任何接口)很簡單:Kotlin 會自動讓您用 lambda 替換它。但對於具備多個方法的監聽器來講,狀況並不是如此。html

所以,在本文中,我想向您展現處理問題的不一樣方法,您甚至能夠在途中學習一些新的 Kotlin 技巧前端

問題所在

當咱們處理監聽器時,咱們知道 OnclickListener 做用於視圖,歸功於 Kotlin 對 Java 庫的優化,咱們能夠將如下代碼:android

view.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
        toast("View clicked!")
    }
})
複製代碼

轉化爲這樣:ios

view.setOnClickListener { toast("View clicked!") }
複製代碼

問題在於,當咱們習慣它時,咱們但願它可以無處不在。然而當接口存在多個方法時,這種作法將再也不適用。git

例如,若是咱們想爲視圖動畫設置一個監聽器,咱們最終獲得如下「漂亮」的代碼:github

view.animate()
        .alpha(0f)
        .setListener(object : Animator.AnimatorListener {
            override fun onAnimationStart(animation: Animator?) {
                toast("Animation Start")
            }

            override fun onAnimationRepeat(animation: Animator?) {
                toast("Animation Repeat")
            }

            override fun onAnimationEnd(animation: Animator?) {
                toast("Animation End")
            }

            override fun onAnimationCancel(animation: Animator?) {
                toast("Animation Cancel")
            }
        })
複製代碼

你可能會反駁說 Android framework 已經爲它提供了一個解決方案:適配器。對於幾乎任何具備多個方法的接口,它們都提供了一個抽象類,將全部方法實現爲空。在上述例子中,您能夠這樣:後端

view.animate()
        .alpha(0f)
        .setListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator?) {
                toast("Animation End")
            }
        })
複製代碼

好的,是改善了一些,但這存在幾個問題:bash

  • 適配器是類,這意味着若是咱們想要一個類做爲此適配器的實現,它不能擴展其餘任何東西。
  • 咱們把一個本能夠用 lambda 清晰表達的事物,變成了一個具備一個方法的匿名對象。

咱們有什麼選擇?ide

Kotlin 中的接口:它們能夠包含代碼

還記得咱們談到 Kotlin 中的接口嗎? 它們內部能夠包含代碼,所以,您可以聲明能夠實現而不是繼承適配器(以防您如今將其用於 Android 開發中,您可使用 Java 8 和接口中的默認方法執行相同的操做):函數

interface MyAnimatorListenerAdapter : Animator.AnimatorListener {
    override fun onAnimationStart(animation: Animator) = Unit
    override fun onAnimationRepeat(animation: Animator) = Unit
    override fun onAnimationCancel(animation: Animator) = Unit
    override fun onAnimationEnd(animation: Animator) = Unit
}
複製代碼

有了這個,默認狀況下全部方法都不會執行任何操做,這意味着一個類能夠實現此接口並僅聲明它所需的方法:

class MainActivity : AppCompatActivity(), MyAnimatorListenerAdapter {
    ...
    override fun onAnimationEnd(animation: Animator) {
        toast("Animation End")
    }
}
複製代碼

以後,您能夠將它做爲監聽器的參數:

view.animate()
        .alpha(0f)
        .setListener(this)
複製代碼

這個方案解決了開始時提出的一個問題,可是咱們仍然要顯式地聲明它。若是我想使用 lambda 表達式呢?

此外,雖然這可能會不時地使用繼承,但在大多數狀況下,您仍將使用匿名對象,這與使用 framework 適配器並沒有不一樣。

可是,這是一個有趣的想法:若是你須要爲具備多個方法的監聽器定義一種適配器,那麼最好使用接口而不是抽象類繼承 FTW 的構成

通常狀況下的擴展功能

讓咱們轉向更加簡潔的解決方案。可能會碰到這種狀況(如上所述):大多數時候你只須要相同的功能,而對另外一個功能則不太感興趣。對於 AnimatorListener,最經常使用的一個方法一般是 onAnimationEnd。那麼爲何不建立一個涵蓋這種狀況的擴展方法呢?

view.animate()
        .alpha(0f)
        .onAnimationEnd { toast("Animation End") }
複製代碼

真棒!擴展函數應用於 ViewPropertyAnimator,這是 animate()alpha 和全部其餘動畫方法返回的內容。

inline fun ViewPropertyAnimator.onAnimationEnd(crossinline continuation: (Animator) -> Unit) {
    setListener(object : AnimatorListenerAdapter() {
        override fun onAnimationEnd(animation: Animator) {
            continuation(animation)
        }
    })
}
複製代碼

以前已經談過 內聯,但若是你還有一些疑問,我建議你看一下官方的文檔

如您所見,該函數只接收在動畫結束時調用的 lambda。這個擴展函數爲咱們完成了建立適配器並調用 setListener 這種不友好的工做。

這樣就好多了!咱們能夠在監聽器中爲每一個方法建立一個擴展方法。但在這種特殊狀況下,咱們遇到了動畫只接受一個監聽器的問題。所以咱們一次只能使用一個。

在任何狀況下,對於大多數重複的狀況(像上面那樣),它並不會損害到像如上提到的 Animator 自己的方法。這是更簡單的解決方案,很是易於閱讀和理解。

使用命名參數和默認值

可是你和我喜歡 Kotlin 的緣由之一是它有不少使人驚奇的功能來簡化咱們的代碼!因此你能夠想象咱們還有一些選擇的餘地。接下來咱們將使用命名參數:這容許咱們定義 lambda 表達式並明確說明它們的用途,這將極大地提升代碼的可讀性。

咱們會有相似於上面的功能,但涵蓋全部方法的狀況:

inline fun ViewPropertyAnimator.setListener(
        crossinline animationStart: (Animator) -> Unit,
        crossinline animationRepeat: (Animator) -> Unit,
        crossinline animationCancel: (Animator) -> Unit,
        crossinline animationEnd: (Animator) -> Unit) {

    setListener(object : AnimatorListenerAdapter() {
        override fun onAnimationStart(animation: Animator) {
            animationStart(animation)
        }

        override fun onAnimationRepeat(animation: Animator) {
            animationRepeat(animation)
        }

        override fun onAnimationCancel(animation: Animator) {
            animationCancel(animation)
        }

        override fun onAnimationEnd(animation: Animator) {
            animationEnd(animation)
        }
    })
}
複製代碼

方法自己不是很好,但一般是伴隨擴展方法的狀況。他們隱藏了 framework 很差的部分,因此有人必須作艱苦的工做。如今您能夠像這樣使用它:

view.animate()
        .alpha(0f)
        .setListener(
                animationStart = { toast("Animation start") },
                animationRepeat = { toast("Animation repeat") },
                animationCancel = { toast("Animation cancel") },
                animationEnd = { toast("Animation end") }
        )
複製代碼

感謝命名參數,讓咱們能夠很清楚這裏發生了什麼。

你須要確保沒有命名參數的時候就不要使用它,不然它會變得有點亂:

view.animate()
        .alpha(0f)
        .setListener(
                { toast("Animation start") },
                { toast("Animation repeat") },
                { toast("Animation cancel") },
                { toast("Animation end") }
        )
複製代碼

不管如何,這個解決方案仍然迫使咱們實現全部方法。但它很容易解決:只需使用參數的默認值。空的 lambda 表達式將上面的代碼演變成:

inline fun ViewPropertyAnimator.setListener(
        crossinline animationStart: (Animator) -> Unit = {},
        crossinline animationRepeat: (Animator) -> Unit = {},
        crossinline animationCancel: (Animator) -> Unit = {},
        crossinline animationEnd: (Animator) -> Unit = {}) {

    ...
}
複製代碼

如今你能夠這樣作:

view.animate()
        .alpha(0f)
        .setListener(
                animationEnd = { toast("Animation end") }
        )
複製代碼

還不錯,對吧?雖然比以前的作法要稍微複雜一點,但卻更加靈活了。

殺手鐗操做:DSL

到目前爲止,我一直在解釋簡單的解決方案,誠實地說可能涵蓋大多數狀況。但若是你想發瘋,你甚至能夠建立一個讓事情變得更加明確的小型 DSL。

這個想法 來自 Anko 如何實現一些偵聽器,它是建立一個實現了一組接收 lambda 表達式的方法幫助器。這個 lambda 將在接口的相應實現中被調用。我想首先向您展現結果,而後解釋使其實現的代碼:

view.animate()
        .alpha(0f)
        .setListener {
            onAnimationStart {
                toast("Animation start")
            }
            onAnimationEnd {
                toast("Animation End")
            }
        }
複製代碼

看到了嗎? 這裏使用了一個小型的 DSL 來定義動畫監聽器,咱們只需調用咱們須要的功能便可。對於簡單的行爲,這些方法能夠是單行的:

view.animate()
        .alpha(0f)
        .setListener {
            onAnimationStart { toast("Start") }
            onAnimationEnd { toast("End") }
        }
複製代碼

這相比於以前的解決方案有兩個優勢:

  • 它更加簡潔:您在這裏保存了一些特性,但老實說,僅僅由於這個還不值得努力。
  • 它更加明確:它迫使開發人員說出他們所重寫的功能。在前一個選擇中,由開發人員設置命名參數。這裏沒有選擇,只能調用該方法。

因此它本質上是一個不太容易出錯的解決方案。

如今來實現它。首先,您仍須要一個擴展方法:

fun ViewPropertyAnimator.setListener(init: AnimListenerHelper.() -> Unit) {
    val listener = AnimListenerHelper()
    listener.init()
    this.setListener(listener)
}
複製代碼

這個方法只獲取一個帶有接收器的 lambda 表達式,它應用於一個名爲 AnimListenerHelper 的新類。它建立了這個類的一個實例,使它調用 lambda 表達式,並將實例設置爲監聽器,由於它正在實現相應的接口。讓咱們看看如何實現 AnimeListenerHelper

class AnimListenerHelper : Animator.AnimatorListener {
    ...
}
複製代碼

而後對於每一個方法,它須要:

  • 保存 lambda 表達式的屬性
  • DSL 方法,它接收在調用原始接口的方法時執行的 lambda 表達式
  • 在原有接口基礎上重寫方法
private var animationStart: AnimListener? = null

fun onAnimationStart(onAnimationStart: AnimListener) {
    animationStart = onAnimationStart
}

override fun onAnimationStart(animation: Animator) {
    animationStart?.invoke(animation)
}
複製代碼

這裏我使用的是 AnimListener 的一個 類型別名

private typealias AnimListener = (Animator) -> Unit
複製代碼

這裏是完整的代碼:

fun ViewPropertyAnimator.setListener(init: AnimListenerHelper.() -> Unit) {
    val listener = AnimListenerHelper()
    listener.init()
    this.setListener(listener)
}

private typealias AnimListener = (Animator) -> Unit

class AnimListenerHelper : Animator.AnimatorListener {

    private var animationStart: AnimListener? = null

    fun onAnimationStart(onAnimationStart: AnimListener) {
        animationStart = onAnimationStart
    }

    override fun onAnimationStart(animation: Animator) {
        animationStart?.invoke(animation)
    }

    private var animationRepeat: AnimListener? = null

    fun onAnimationRepeat(onAnimationRepeat: AnimListener) {
        animationRepeat = onAnimationRepeat
    }

    override fun onAnimationRepeat(animation: Animator) {
        animationRepeat?.invoke(animation)
    }

    private var animationCancel: AnimListener? = null

    fun onAnimationCancel(onAnimationCancel: AnimListener) {
        animationCancel = onAnimationCancel
    }

    override fun onAnimationCancel(animation: Animator) {
        animationCancel?.invoke(animation)
    }

    private var animationEnd: AnimListener? = null

    fun onAnimationEnd(onAnimationEnd: AnimListener) {
        animationEnd = onAnimationEnd
    }

    override fun onAnimationEnd(animation: Animator) {
        animationEnd?.invoke(animation)
    }
}
複製代碼

最終的代碼看起來很棒,但代價是作了不少工做。

我該使用哪一種方案?

像往常同樣,這要看狀況。若是您不在代碼中常用它,我會說哪一種方案都不要使用。在這些狀況下要根據實際狀況而定,若是你要編寫一次監聽器,只需使用一個實現接口的匿名對象,並繼續編寫重要的代碼。

若是您發現須要使用更屢次監聽器,請使用其中一種解決方案進行重構。我一般會選擇只使用咱們感興趣的功能進行簡單的擴展。若是您須要多個監聽器,請評估兩種最新替代方案中的哪種更適合您。像往常同樣,這取決於你將要如何普遍地使用它。

但願這篇文章可以在您下一次處於這種狀況下時幫助到您。若是您以不一樣方式解決此問題,請在評論中告訴咱們!

感謝您的閱讀 🙂

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


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

相關文章
相關標籤/搜索