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

image

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

因此,在本文中,我想向您展示處理問題的不同方法,您甚至可以在途中學習一些新的 Kotlin 技巧

問題所在

當我們處理監聽器時,我們知道 OnclickListener 作用於視圖,歸功於 Kotlin 對 Java 庫的優化,我們可以將以下代碼:

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

轉化爲這樣:

view.setOnClickListener { toast("View clicked!") }

問題在於,當我們習慣它時,我們希望它能夠無處不在。然而當接口存在多個方法時,這種做法將不再適用。

例如,如果我們想爲視圖動畫設置一個監聽器,我們最終得到以下“漂亮”的代碼:

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")
            }
        })

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

  • 適配器是類,這意味着如果我們想要一個類作爲此適配器的實現,它不能擴展其他任何東西。
  • 我們把一個本可以用 lambda 清晰表達的事物,變成了一個具有一個方法的匿名對象。

我們有什麼選擇?

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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章