Single-function listeners using lambda

Viewed 285

With all the well-known single-function listeners we can use a simpler lambda notation

view.setOnClickListener { do() }

instead of the original, longer Java way of

view.setOnClickListener(object : View.OnClickListener {
  override fun onClick(v: View?) {
    do()
  }
})

But what exactly makes this work? I tried to do the same with my own listener:

private var listener: OnCopyPasteClickListener? = null

interface OnCopyPasteClickListener {
  fun onPasteClick(text: String)
}

fun setOnCopyPasteClickListener(onCopyPasteClickListener: OnCopyPasteClickListener) {
  listener = onCopyPasteClickListener
}

and while the long approach works just fine:

copypaste.setOnCopyPasteClickListener(object : CopyPasteMenu.OnCopyPasteClickListener {
  override fun onPasteClick(text: String) {
    do(text)
  }
})

I can't make it accept the short one:

copypaste.setOnCopyPasteClickListener {
  do(it)
}

The IDE gives a type mismatch error.

3 Answers

Actually, if you have only one function to be invoked, I recommend you use Kotlin Callback.

typealias OnDoWorkListener = ((String) -> Unit)

class Work {
    var doWork: OnDoWorkListener? = null

    fun doSomething() {
       doWork?.invoke("Message Here")
    }
}

And in your function, you just set the callback to it

fun main() {
   val work = Work()

   work.doWork = {
       Log.d("WORK", "This gets called from the `work` object. Message: $it")
   }

   work.doSomething();
}

We can also use function to set the listener as well.

class Work {
    var doWork: OnDoWorkListener? = null

    fun doSomething() {
       doWork?.invoke("Message Here")
    }

    fun setOnWorkListener(listener: OnDoWorkListener) {
       doWork = listener
    }
}


fun main() {
   val work = Work()
   work.setOnWorkListener { 
       Log.d("WORK", "This gets called from the `work` object. Message: $it")
   }
   work.doSomething()
}

Higher order functions make this work:

Kotlin functions are first-class, which means that they can be stored in variables and data structures, passed as arguments to and returned from other higher-order functions. You can operate with functions in any way that is possible for other non-function values.

From the same page:

Passing a lambda to the last parameter

In Kotlin, there is a convention that if the last parameter of a function accepts a function, a lambda expression that is passed as the corresponding argument can be placed outside the parentheses:

val product = items.fold(1) { acc, e -> acc * e }

If the lambda is the only argument to that call, the parentheses can be omitted entirely:

run { println("...") }

Knowing this, a possible update on your class would look like:

class CopyPaste {
    private var listener: (String) -> Unit = {}

    fun setOnCopyPasteClickListener(onCopyPasteClickListener: (String) -> Unit) {
        listener = onCopyPasteClickListener
    }

    fun doCopyPaste(value: String) {
        listener.invoke(value)
    }
}

fun main() {
    val copyPaste = CopyPaste()
    copyPaste.setOnCopyPasteClickListener { println(it) }
    copyPaste.doCopyPaste("ClipboardContent!")
}

The class CopyPaste stores the listener, which is a function that takes a String parameter and does not return anything. Its function setOnCopyPasteClickListener accepts a function with the same signature as the listener property and at the end doCopyPaste accepts a String parameter and passes it to the stored function.

Actually, just after I posted, I searched for more thoughts and found this thread: https://youtrack.jetbrains.com/issue/KT-7770 This is indeed a debated limitation as it currently only applies to Java, not Kotlin itself. There is also a suggestion there that gives almost the required simplicity:

interface OnCopyPasteClickListener {
  fun onPasteClick(text: String)

  companion object {
    inline operator fun invoke(crossinline op: (text: String) -> Unit) =
      object : OnCopyPasteClickListener {
        override fun onPasteClick(text: String) = op(text)
      }
  }
}

and then, thanks to this overloaded operator, it can be called as:

copypaste.setOnCopyPasteClickListener(CopyPasteMenu.OnCopyPasteClickListener { text ->
  do(text)
})

But as the suggested answers offer a more idiomatic solution, I'll accept one of those, I only wanted to include this approach here for reference.

Related