- 原文地址:Listeners with several functions in Kotlin. How to make them shine?
- 原文做者:Antonio Leiva
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Moosphon
- 校對者:Qiuk17, zx-Zhu
我常常遇到的一個問題是在使用 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
咱們有什麼選擇?ide
還記得咱們談到 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。
這個想法 來自 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 {
...
}
複製代碼
而後對於每一個方法,它須要:
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 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。