当 Kotlin 中的监听器包含多个方法时,如何让它 “巧夺天工”?

栏目: IOS · 发布时间: 5年前

内容简介:我经常遇到的一个问题是在使用 Kotlin 时如何简化具有多个方法的监听器的交互。对于具有只具有一个方法的监听器(或任何接口)很简单:Kotlin 会自动让您用 lambda 替换它。但对于具有多个方法的监听器来说,情况并非如此。因此,在本文中,我想向您展示处理问题的不同方法,您甚至可以在途中学习一些新的 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前端后端区块链产品设计人工智能 等领域,想要查看更多优质译文请持续关注 掘金翻译计划 、官方微博、 知乎专栏


以上所述就是小编给大家介绍的《当 Kotlin 中的监听器包含多个方法时,如何让它 “巧夺天工”?》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Data Structures and Algorithm Analysis in Java

Data Structures and Algorithm Analysis in Java

Mark A. Weiss / Pearson / 2006-3-3 / USD 143.00

As the speed and power of computers increases, so does the need for effective programming and algorithm analysis. By approaching these skills in tandem, Mark Allen Weiss teaches readers to develop wel......一起来看看 《Data Structures and Algorithm Analysis in Java》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具