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

沐風同學發表於2018-12-22

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

當 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 連結。


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

相關文章