• Listeners with several functions in Kotlin. How to make them shine?
  • Antonio Leiva
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Moosphon
  • Proofreader: Qiuk17, ZX-Zhu

How do you make a listener in Kotlin “neat” when it contains multiple methods?

One problem I often encounter is how to simplify the interaction of listeners with multiple methods when using Kotlin. Having a listener (or any interface) that has only one method is simple: Kotlin will automatically let you replace it with a lambda. This is not the case for listeners with multiple methods.

So in this article, I want to show you different ways to handle problems, and you can even learn some new Kotlin tricks along the way!

The problem

When we deal with listeners, we know that OnclickListener applies to the view, thanks to Kotlin’s optimization of the Java library, and we can add the following code:

view.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
        toast("View clicked!")}})Copy the code

This translates to:

view.setOnClickListener { toast("View clicked!")}Copy the code

The problem is, when we get used to it, we want it to be everywhere. However, when there are multiple methods on the interface, this approach is no longer applicable.

For example, if we want to set up a listener for the view animation, we end up with the following “nice” code:

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")}})Copy the code

You might argue that the Android Framework already provides a solution for it: an adapter. For almost any interface with multiple methods, they provide an abstract class that implements all methods as null. In the example above, you could:

view.animate()
        .alpha(0f)
        .setListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator?) {
                toast("Animation End")}})Copy the code

Well, it’s improved a bit, but there are several problems with that:

  • Adapters are classes, which means that if we want a class to be an implementation of this adapter, it cannot extend anything else.
  • We took something that could have been articulated in lambda and turned it into an anonymous object with a method.

What are our options?

Interfaces in Kotlin: They can contain code

Remember when we talked about interfaces in Kotlin? They can contain code internally, so you can declare that you can implement the adapter instead of inheriting it (in case you’re using it for Android development now, you can do the same using Java 8 and the default methods in the interface) :

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
}
Copy the code

With this, none of the methods do anything by default, which means that a class can implement this interface and declare only the methods it needs:

class MainActivity : AppCompatActivity(), MyAnimatorListenerAdapter {
    ...
    override fun onAnimationEnd(animation: Animator) {
        toast("Animation End")}}Copy the code

After that, you can use it as an argument to the listener:

view.animate()
        .alpha(0f)
        .setListener(this)
Copy the code

This solution solves an initial problem, but we still have to declare it explicitly. What if I want to use lambda expressions?

Also, while this might use inheritance from time to time, in most cases you’ll still use anonymous objects, which is no different than using a Framework adapter.

However, this is an interesting idea: if you need to define an adapter for listeners with multiple methods, it is better to use interfaces rather than abstract classes. Inherit the composition of FTW.

Extended functionality in general

Let’s move on to a cleaner solution. It’s possible (as described above) that most of the time you just need the same feature and aren’t very interested in the other one. For AnimatorListener, one of the most common methods is usually onAnimationEnd. So why not create an extension method that covers this situation?

view.animate()
        .alpha(0f)
        .onAnimationEnd { toast("Animation End")}Copy the code

That’s great! Extension functions are applied to ViewPropertyAnimator, which is returned by animate(), alpha, and all other animation methods.

inline fun ViewPropertyAnimator.onAnimationEnd(crossinline continuation: (Animator) -> Unit) {
    setListener(object : AnimatorListenerAdapter() {
        override fun onAnimationEnd(animation: Animator) {
            continuation(animation)
        }
    })
}
Copy the code

I’ve talked about inlining before, but if you have any questions, I suggest you take a look at the official documentation.

As you can see, this function only accepts lambdas that are called at the end of the animation. This extension function does the nasty work of creating the adapter and calling setListener for us.

That’s better! We can create an extension method for each method in the listener. But in this particular case, we run into a problem where the animation accepts only one listener. So we can only use one at a time.

In any case, it does not compromise the methods of the Animator itself as mentioned above for most repetitive cases (like the one above). This is a simpler solution that is very easy to read and understand.

Use named parameters and default values

But one of the things you and I like about Kotlin is that it has so many amazing features to simplify our code! So you can imagine we have some options. Next we’ll use named parameters: This allows us to define lambda expressions and specify their purpose, which greatly improves the readability of the code.

We would have a case where the functionality is similar to the above, but covers all methods:

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)
        }
    })
}
Copy the code

The method itself is not very good, but it is usually accompanied by an extension method. They hide the bad parts of the framework, so someone has to do the hard work. Now you can use it like this:

view.animate()
        .alpha(0f)
        .setListener(
                animationStart = { toast("Animation start") },
                animationRepeat = { toast("Animation repeat") },
                animationCancel = { toast("Animation cancel") },
                animationEnd = { toast("Animation end")})Copy the code

Thanks to the named parameter, it’s clear what’s going on here.

You need to make sure you don’t use it without named parameters, otherwise it will get a bit messy:

view.animate()
        .alpha(0f)
        .setListener(
                { toast("Animation start") },
                { toast("Animation repeat") },
                { toast("Animation cancel") },
                { toast("Animation end")})Copy the code

However, this solution still forces us to implement all methods. But it’s easy to fix: just use the default values of the parameters. The empty lambda expression evolves the above code to:

inline fun ViewPropertyAnimator.setListener(
        crossinline animationStart: (Animator) -> Unit = {},
        crossinline animationRepeat: (Animator) -> Unit = {},
        crossinline animationCancel: (Animator) -> Unit = {},
        crossinline animationEnd: (Animator) -> Unit = {}) {

    ...
}
Copy the code

Now you can do this:

view.animate()
        .alpha(0f)
        .setListener(
                animationEnd = { toast("Animation end")})Copy the code

Not bad, right? It’s a little more complicated than before, but it’s more flexible.

Killer operation: DSL

So far, I’ve been explaining simple solutions that honestly probably cover most cases. But if you want to go crazy, you can even create a small DSL that makes things clearer.

The idea comes from how Anko implements some listeners, which create a helper that implements a set of methods that receive lambda expressions. This lambda will be called in the corresponding implementation of the interface. I want to show you the results first, and then explain the code that makes it happen:

view.animate()
        .alpha(0f)
        .setListener {
            onAnimationStart {
                toast("Animation start")
            }
            onAnimationEnd {
                toast("Animation End")}}Copy the code

See? Here we use a small DSL to define the animation listener, and we just need to call the functionality we need. For simple actions, these methods can be single-line:

view.animate()
        .alpha(0f)
        .setListener {
            onAnimationStart { toast("Start") }
            onAnimationEnd { toast("End")}}Copy the code

This has two advantages over previous solutions:

  • It’s cleaner: You save some features here, but honestly, just because it’s not worth the effort.
  • It’s more explicit: it forces developers to say what features they’re rewriting. In the former option, the developer sets the named parameters. There is no choice but to call this method.

So it’s essentially a less error-prone solution.

Now let’s do it. First, you still need an extension method:

fun ViewPropertyAnimator.setListener(init: AnimListenerHelper.() -> Unit) {
    val listener = AnimListenerHelper()
    listener.init()
    this.setListener(listener)
}
Copy the code

This method simply takes a lambda expression with a receiver, which is applied to a new class called AnimListenerHelper. It creates an instance of this class, makes it invoke a lambda expression, and sets the instance as a listener because it is implementing the corresponding interface. Let’s see how to implement AnimeListenerHelper:

class AnimListenerHelper : Animator.AnimatorListener {
    ...
}
Copy the code

Then for each method, it needs:

  • Saves the attributes of a lambda expression
  • DSL methods, which receive lambda expressions that are executed when the methods of the original interface are called
  • Override methods based on the original interface
private var animationStart: AnimListener? = null fun onAnimationStart(onAnimationStart: AnimListener) { animationStart = onAnimationStart } override fun onAnimationStart(animation: Animator) { animationStart? .invoke(animation) }Copy the code

Here I’m using a type alias for AnimListener:

private typealias AnimListener = (Animator) -> Unit
Copy the code

Here’s the complete code:

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) } }Copy the code

The final code looks great, but at the cost of a lot of work.

Which plan should I use?

As usual, it depends. If you don’t use it often in your code, I’d say don’t use either solution. In these cases it depends; if you’re writing a listener once, just use an anonymous object that implements the interface and keep writing the important code.

If you find that you need to use the listener more than once, use one of these solutions for refactoring. I usually opt for simple extensions that only use features we’re interested in. If you need more than one listener, evaluate which of the two latest alternatives is best for you. As always, it depends on how widely you want to use it.

Hopefully this article will help you the next time you find yourself in such a situation. If you solve this problem differently, let us know in the comments!

Thank you for reading 🙂

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.