- 原文地址: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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。