1. Introduction

In Kotlin, a callback function is one that is passed as an argument to another function. During the execution of the receiving function, the callback function can then be invoked at the appropriate time.

In this article, we’re going to learn more about callback functions in Kotlin.

2. Defining Callback Functions in Kotlin

We define callback functions in Kotlin by means of lambda expressions. Lambdas Expressions have no name and, as such, are referred to as anonymous functions.

Basically, a lambda expression takes the form:

val lambdaName : Type = { argumentList -> codeBody }

It’s important to mention that every part of the lambda expression is optional except for the body, which must be specified. Such an expression is qualified as an argument for any function.

From the declaration above, the argumentList represents the list of parameters that the lambda expression accepts, while the codeBody represents the actual code that the lambda expression executes. The -> operator separates the argumentList and the codeBody.

For instance, let’s define a lambda expression that calculates the least common multiple of two integers:

val lcm = {x: Int, y: Int ->
    var gcd = 1
    var i = 1
    while (i <= x && i <= y) {
        if (x % i == 0 && y % i == 0)
            gcd = i
        ++i
    }
    x * y / gcd
}

The lcm lambda expression accepts two integer parameters, x and y, and returns their lowest common multiple.

We can use this lambda expression by passing it as an argument to another useful higher-order function: reduce().

The reduce() function takes a lambda function operator to combine a pair of elements into a so-called accumulated value. In our case, the reduce() function traverses the collection of Ints from left to right and stepwise combines the accumulated least common multiple with the next element:

@Test
fun `callback function to perform LCM`(){
    var res1 = listOf(2,3,4).reduce(lcm)
    var res2 = listOf(5,15,4).reduce(lcm)

    assertEquals(12, res1)
    assertEquals(60, res2)
}

3. Why Are Callback Functions Useful?

Callbacks can be used to illustrate functional programming concepts in Kotlin. In functional programming, functions can be wielded just like variables. We can assign anonymous functions to variables:

var square = fun (x: Int) : Int{
    return x*x
}
assertEquals(16, square(4))

3.1. Function Composition

Just like strings and integers, we can pass functions into other functions. This creates higher-order functions. Higher-order functions are good for composition as they permit us to compose a lot of small functions into bigger ones.

As an example, let’s look into a useful higher-order function in Kotlin: filter(). The filter() function on an Array accepts a callback function as an argument that will be used to return a filtered version of the Array on which it is called:

var numbers = arrayOf(1,2,3,4,5,6,7,8,9,10)
var evenNumbers = numbers.filter( fun(number): Boolean {
    return number % 2 == 0
})
assertContentEquals(arrayOf(2, 4, 6, 8, 10).toIntArray(), evenNumbers.toIntArray())

In this example, we pass a callback function to filter(). The filter() function iterates through the list of numbers and, for each number, calls the callback function on it.

3.2. Asynchronous Callbacks

Another interesting use of callbacks is asynchronous programming. When it comes to Kotlin, callback functions are at the core of asynchronous code execution. Unquestionably, asynchronous programming comes with plenty of perks, as it permits developers to split the execution of a program across multiple threads that run independently of one another.

The power of asynchronous execution of code is evident when dealing with long-running operations in our programs. They give us a way to avoid blocking the main thread by pushing some code execution on background or worker threads.

In addition, callback functions are great for event handling. An event represents the change of state of an object by performing an action, such as handling user inputs or scheduling a timer.

4. Asynchronous Callback Functions

As already mentioned, callback functions in Kotlin play a core role in administering asynchronous techniques in our programs. With a callback function, we can execute code pieces asynchronously, as well as execute a certain piece of code when a particular event occurs.

Let’s consider an example where we want to pull some data from the server off the main thread. To achieve this, we can use a callback function that executes the remote loading of users from a server asynchronously:

data class User(var firstName: String, var lastName: String)
suspend fun loadUsersFromServer(callback: (List<User>) -> Unit) {
    delay(5000)
    val users = listOf(User("Flore", "P"), User("Nappy", "Sean"), User("Ndole", "Paul"))
    callback(users)
}

In this example, loadUsersFromServer() accepts a callback function as an argument. The callback function accepts a list of Users as an argument and returns a Unit (Nothing). Just for simulation purposes, we sleep this thread for five seconds and then invoke the callback function on a list of users.

We can now use the loadUserFromServer() method and invoke the callback function:

var listofUsers = emptyList<User>()
suspend fun executeLoading(){
    loadUsersFromServer { users ->
        listofUsers = users
    }
}

Now, a complete test of these functions will look like this:

@Test
fun `asynchronous callback to load remote data`(){
    runBlocking{
        executeLoading()
    }
    assertEquals(3, listofUsers.size)
    assertEquals("Flore", listofUsers[0].firstName)
    assertEquals("Sean", listofUsers[1].lastName)
    assertEquals("Ndole Paul", "${listofUsers[2].firstName} ${listofUsers[2].lastName}" )
}

5. Pitfalls of Callback Functions

Using callback functions in our programs comes with a great number of perks, such as handling events and asynchronous execution of code. However, the inordinate use of callback functions in our programs can become counterproductive.

The phenomenon that happens when we nest multiple callback functions is known as “callback hell”. It makes our code difficult to understand and maintain.

To better understand this phenomenon of callback hell, assume we need to complete a task, say Z. To achieve task Z, we need to accomplish a couple of dependent tasks asynchronously ranging from task W to Y. Let’s look at this concept in pseudocode:

To Compute Z
Do W -> get results for W
    Do X -> get results for X
         Do Y -> get results for Y
              Then Do Z

By completing one task and using its result in the next task, we end up with a chain of tasks that yields nested callback functions.

Now, let’s consider a concrete example. We want to download some books via the downloadBook() method. This method then gets the user token, proceeds to save the book, and executes the openBook() method:

fun getUserToken(id: Int, callback: (id:Int) -> Int): Int{
    return callback(id)
}
fun hashUserToken(id:Int): Int{
    return (id%100) * 12000
}
fun downloadBook(callback: (id: Int) -> Unit){
    var userToken = getUserToken(2){
        id ->  hashUserToken(id)
    }
    callback(userToken)
}
fun saveBook(bookId: Int, callback: (bookId: Int) -> Unit){
    callback(bookId)
}
fun openBook(bookId: Int): Boolean{
    if (bookId>0)
        return true
    
    return false
}
fun openPDF(bookId: Int){
    downloadBook { id ->
        saveBook(bookId){bookId ->
            openBook(bookId)
        }
    }
}

From the openPdf() method, we see how we can chain different tasks. First, we download the book, which requires that we obtain a user token. Then, we save the book in storage. Lastly, we open the book. The openPdf() method is cluttered with a lot of nested callbacks, and that is an example of a callback hell.

To avoid running into a callback hell, there are some libraries readily available — such as coroutines with suspend functions — that developers can leverage.

6. Conclusion

In this article, we’ve explored the concept of callback functions in Kotlin. We’ve also seen how important callback functions are in the asynchronous execution of code, as well as event handling, with practical code examples. We ended up describing a major pitfall of callbacks: callback hell.

As usual, all code samples pertaining to this tutorial are available over on GitHub.