- 原文地址:Listeners with several functions in Kotlin. How to make them shine?
- 原文作者:Antonio Leiva
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:Moosphon
- 校對者:Qiuk17, zx-Zhu
當 Kotlin 中的監聽器包含多個方法時,如何讓它 “巧奪天工”?
我經常遇到的一個問題是在使用 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 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。