Single-function listeners using lambda - kotlin

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.

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

How to define a method only when class generic type satisfies a test?

I am trying to do something like the following:
class Event<TPayload>() {
fun subscribe(handler: (payload: TPayload) -> Unit) { ... }
fun subscribe(handler: () -> Unit) where TPayload : Unit { ... }
}
The intention is that instances of Event<Unit> will have two overloads of subscribe(), but other instances will only have one.
The above code will not compile. I tried using extension methods, but would have to use a different name for the extra method, rather than overloading it.
You can define that second function as an extension function so it only appears for Events who have a type of Unit. It's okay to overload the function name. Define it outside the class:
inline fun Event<Unit>.subscribe(crossinline handler: ()->Unit) =
subscribe { handler() }
Test:
class Event<T> {
private val subscribers = mutableListOf<(T)->Unit>()
fun subscribe(handler: (payload: T) -> Unit) {
subscribers += handler
}
fun send(payload: T) {
for (subscriber in subscribers) subscriber(payload)
}
}
fun main() {
val event = Event<Unit>()
// Using verbose syntax to prove it's the extension function being used
// and not a lambda with implicit 'it':
event.subscribe(fun() { println("got unit") })
event.send(Unit)
}
If you use a lambda, the compiler will use the first subscribe function with an implicit it parameter since it takes precedence in overload resolution. But runtime behavior would be the same either way if you aren't using the parameter.

Kotlin compiler reports unused expression in constructor for builder taking vararg lambdas

We have a relatively simple builder pattern we use for test data generator in Kotlin.
The builders follow the pattern:
class ThingBuilder private constructor(
var param1: Int = 1,
var param2: Boolean = true
) {
private constructor(vararg inits: ThingBuilder.(ThingBuilder) -> Unit) : this() {
inits.forEach { it(this) }
}
fun build(): Thing {
return Thing(
param1,
param2
)
}
companion object {
fun asDefaultCase(init: ThingBuilder.(ThingBuilder) -> Unit = {}): ThingBuilder {
return ThingBuilder(init)
}
fun asSomethingElseCase(init: ThingBuilder.(ThingBuilder) -> Unit = {}): ThingBuilder {
return ThingBuilder({ b -> b.param2 = false }, init)
}
}
}
Here the Kotlin compiler reports a warning:
The expression is unused
which references the line:
inits.forEach { it(this) }
I've tried turning that into an Array<T> rather than varags but same warning occurs.
What would be the more correct way to make this structure where the consumers can pass in lambdas to configure the builder data?
(for reference, the code works correctly and the loop functions as expected)
This seems to be a rather old bug KT-21282 False positive UNUSED_EXPRESSION compiler warning with object and lambda with receiver / extension function type.
The fix is simple - just specify the explicit receiver and do this.it(this). I also don't see why you would need to pass this as both the receiver and the formal parameter to the block. I would just do this instead:
private constructor(vararg inits: ThingBuilder.() -> Unit) : this() {
inits.forEach { this.it() }
}
or:
private constructor(vararg inits: ThingBuilder.() -> Unit) : this() {
inits.forEach { it(this) }
}
Then you don't even need to write the b parameter in asSomethingElseCase:
fun asSomethingElseCase(init: ThingBuilder.() -> Unit = {}): ThingBuilder {
return ThingBuilder({ param2 = false }, init)
}

Pass a list of functions with different parameters in Kotlin

I have a class that calls functions depending on events. Events are emitted from sockets. I should catch these events, parse JSON and respond (call a corresponding function). For instance, {"event_name": "message", "data": {"text": "dfgfdgfdg", "sender": "dsfdsfs"}}
fun listener(jsonString: String, methodsMap: Map<String, () -> Unit>) {
val json = JSONObject(jsonString)
val data = json.getJSONObject("data")
when (json.get("event_name")) {
"update" -> {
val count = data.getInt("count")
methodsMap["update"]?.invoke(count) // 1 parameter.
}
"message" -> {
val message = data.getString("text")
val sender = data.getString("sender")
methodsMap["message"]?.invoke(message, sender) // 2 parameters.
}
}
}
So, I cannot create one method that calls functions with different parameters. How to do this?
Since you are already have if-then logic in listener, having the functions in a Map is of questionable value and it forces you to to deal with the fact that your functions are of different types. If it is parametrisation of listener you are after, perhaps this (simplified example code that skips JSON) is sufficient:
class UpdateHandler {
fun update(n: Int) = println("update ( $n )")
}
class MessageHandler {
fun message(s1: String, s2: String) = println("message ( $s1 $s2 )")
}
fun listener(jsonString: String, updateF: (Int) -> Unit, messageF: (String, String) -> Unit) {
when (jsonString) {
"update" -> updateF(73)
"message" -> messageF("message", "sender")
}
}
fun main() {
val updateHandler = UpdateHandler()
val messageHandler = MessageHandler()
val listener = { json: String -> listener(json, updateHandler::update, messageHandler::message) }
listener("update") // prints: update ( 73 )
listener("message")// prints: message ( message sender )
}
First, I wanted to use a list of parameters in each function, but it leads to poor type verification during compilation. Also I wanted to assign vararg instead of List, but couldn't.
fun listener(jsonString: String, methodsMap: Map<String, (List<Any>) -> Unit>) {
...
methodsMap["update"]?.invoke(listOf(count)) // 1 parameter.
...
methodsMap["message"]?.invoke(listOf(message, sender)) // 2 parameters.
}
This is a poor solution. Bugs may occur, we should remember to change methodsMap in every class that uses listener when we change any event.
Second, I tried to use sealed classes. This is not so simple.
Third, I tried to use interface. We know that callbacks are usually made with interfaces. We can even merge interfaces in Kotlin. So, this can be a solution to a problem (but not to a question).
fun listener(jsonString: String, callback: EventListener) {
val json = JSONObject(jsonString)
val data = json.getJSONObject("data")
when (json.get("event_name")) {
"update" -> {
val count = data.getInt("count")
callback.onUpdate(count)
}
"message" -> {
val text = data.getString("text")
val sender = data.getString("sender")
callback.onNewMessage(text, sender)
}
}
}
interface EventListener {
fun onUpdate(count: Int)
fun onNewMessage(text: String, sender: String)
}
Then we can call listener outside of the class and pass any callbacks we like.

Kotlin - Trying to factorize code with high-order function

I'm quite new to Kotlin and I'd like to see if using high-order functions can help in my case.
My use-case is that I need to call the methods of an IInterface derived class to send events to one or more components. And I'd like to make this generic, and I want to check if a high-order funtion can help. A sample of code will help to understand (well, I hope so!).
private val eventListeners = mutableListOf<IEventInterface>() // List filled somewhere else!
private fun sendConnectionEvent(dummyString: String) {
val deadListeners = mutableListOf<IEventInterface>()
eventListeners.forEach {
try {
it.onConnectionEvent(dummyString)
} catch (e: DeadObjectException) {
Log.d(TAG, "Removing listener - Exception ${e.message}")
deadListeners.add(it)
}
}
deadListeners.forEach { it ->
eventListeners.remove(it)
}
}
private fun sendWonderfulEvent(dummyString: String, dummyInt: Int) {
val deadListeners = mutableListOf<IEventInterface>()
eventListeners.forEach {
try {
it.onWonderfulEvent(dummyString, dummyInt)
} catch (e: DeadObjectException) {
Log.d(TAG, "Removing listener - Exception ${e.message}")
deadListeners.add(it)
}
}
deadListeners.forEach { it ->
eventListeners.remove(it)
}
}
I added 2 similar methods (I will have many more in the real use case) and I think (I hope!) that something could be done but I can't make high-order function works in this case because:
I want to call the same method on several instances, and not 'just' a basic function
To make things even worse, the methods I need to call don't have the same prototype (that would have been too easy!).
Hope this is clear enough.
Thanks for your help!
VR
Here is how it can be done
fun onEvent(body: (IEventInterface) -> Unit) {
val deadListeners = mutableListOf<IEventInterface>()
eventListeners.forEach {
try {
body(it)
} catch (ex: DeadObjectException) {
Log.d(TAG, "Removing listener - Exception ${e.message}")
deadListeners.add(it)
}
}
deadListeners.forEach { it ->
eventListeners.remove(it)
}
}
Supposing an interface like this:
interface IEventInterface {
fun onConnectionEvent(dummyString: String)
fun onWonderfulEvent(dummyString: String, dummyInt: Int)
}
Define an generic type that implements your defined interface ( <T : IEventInterface>)
Define an mutable list of this type to receive your implementation (MutableList<T>.removeIfThrows)
Expect an extension function for you type that will do your specific validation (and custom parameters if you want)
Using an apply and returning the instance you can run your code like a pipeline
Executing the custom validation when you want
private fun <T : IEventInterface> MutableList<T>.removeIfThrows(validation: T.() -> Unit, customLogMessage: String? = null): MutableList<T> {
return apply {
removeIf {
it.runCatching {
validation()
}.onFailure { error ->
print(customLogMessage ?: "Removing listener - Exception ${error.message}")
}.isFailure
}
}
}
Define your specific implementation passing just the function with custom validation as an parameter
private fun <T : IEventInterface> MutableList<T>.sendConnectionEvent(dummyString: String) = removeIfThrows({
onConnectionEvent(dummyString)
})
private fun <T : IEventInterface> MutableList<T>.sendWonderfulEvent(dummyString: String, dummyInt: Int) = removeIfThrows({
onWonderfulEvent(dummyString, dummyInt)
})
Now you can run your code like an pipeline modifying your original object like this
private fun nowYouCanDoSomethingLikeThis() {
eventListeners
.sendConnectionEvent("some dummy string")
.sendWonderfulEvent("some another dummy string", 123)
}

Pass function that receives params as param to class extension method

I'm trying to pass a function as parameter to a class extension method.
When doing
fun Router.handleJsonGet(path: String, method: () -> Any) {
this.get(path).handler {
it.response().putHeader("Content-Type", "application/json").end((gson.toJson(method())))
}
}
I can easily wrap a function call inside a lambda and everything's working fine.
router.handleJsonGet("/business/list", { BusinessService.listBusinesses() })
Now I want to include a map of GET params and I'm getting stuck.
fun Router.handleJsonGet(path: String, method: (any:Any) -> Any) {
this.get(path).handler {
val map: MultiMap = it.request().params() ?: MultiMap.caseInsensitiveMultiMap()
it.response().putHeader("Content-Type", "application/json").end((gson.toJson(method(map))))
}
}
How do I make use of this method extension now? The wrap-with-lambda trick doesn't seem to work when the function requires a parameter.
I was expecting something like:
router.handleJsonGet("/business/list", BusinessService::listBusinesses)
with
object BusinessService {
fun listBusinesses(any: Any) : List<Business> {
// do something with any as MultiMap
return Business.findAllBusinesss();
}
}
to work, but IntelliJ says Type Mismatch
Any idea how I should be passing a function that requires a parameter to a class extension method and how I should be calling it?
The syntax you tried to use (BusinessService::listBusinesses) is not yet supported for objects in Kotlin 1.0.4, it is only planned for Kotlin 1.1 (Bound callable references).
Seems like you just need to use lambda with argument, either explicit or implicit it:
router.handleJsonGet("/business/list") { a -> BusinessService.listBusinesses(a) }
router.handleJsonGet("/business/list") { BusinessService.listBusinesses(it) }
Simplified example that demonstrates it:
object BusinessService {
fun listBusinesses(any: Any): List<String> {
return listOf("a", "b", "c");
}
}
fun handleJsonGet(path: String, method: (Any) -> Any) {
val map: Map<String, Int> = mapOf("a" to 1)
method(map)
}
fun main(args: Array<String>) {
handleJsonGet("1") { BusinessService.listBusinesses(it) }
}
Call it like this:
router.handleJsonGet("/business/list", {
BusinessService.listBusinesses(anyParamHere)
})