Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Suspend function

Introduction

In the Network Request example, we concluded that httpsGet() function is a normal function and not suspend function. Due to which, coroutine is unable to suspend (pause) after sending the HTTP request. This leads to blocking behaviour i.e. the thread on which coroutine runs is blocked. Thread can not be used by other coroutine when one coroutine blocks it. To overcome this blocking behaviour, suspend functions are required.

Suspend function is a special function that can be suspended (paused) and resumed.

Suspend functions are defined using the suspend keyword.

 suspend fun /* name */() {

}

// Example :
suspend fun httpGet(url: String): String {
		/* code */
}
 

Suspend functions can be invoked only from a CoroutineScope or another suspend function.

 suspend fun httpGet(url: String): String {
		/* code */
}

// Invalid Direct invocation
fun main() {
		httpGet("example.com") // ERROR! Suspend function can not be invoked directly
}

// Valid invocation from CoroutineScope
fun main() {
		runBlocking { /* this: CoroutineScope */
				httpGet("example.com")
		}
}

// Valid invocation from another suspend function
suspend fun getData(): String {
		return httpGet("example.com")
}
 

Before defining our own suspend functions, let’s use some pre-defined suspend functions first.

delay() function

delay() is a suspend function used to forcefully suspend (pause) the execution of a coroutine for a specific time period.

It takes single parameter - timeMillis i.e. the number of milliseconds to delay the coroutine for.

 suspend fun delay(timeMillis: Long)
 

Example :

 fun main() {
    runBlocking {
        println("delaying for 3 seconds...")
        delay(3000)
        println("printed after 3 seconds")
    }
}

/* Output :

delaying for 3 seconds...
printed after 3 seconds
 */
 

Note that delay() is a suspend function so it can only be invoked from CoroutineScope or another suspend function. Following code won’t work :

 fun main() {
		delay(1500) // ERROR! suspend function
}
 

Example (Loading Animation)

When sending a network request, we can display a simple loading animation on the console. The animation is printing and overwriting a set of characters in a specific order. Here we will use the character set : \ | / -, which when printed in this order would seem like a rotating line.

For overwriting already printed character, we print the backspace character \b. It will omit the last printed character and reset the cursor to previous position.

Iterating over the character set such that on each iteration, character is printed and then omitted for next character to be printed. This technique does not work as expected. Because computer is fast at printing on the console, some characters are overwritten so quickly that they don’t appear. This leads to inconsistency in the animation.

 fun main() {
    val sequence = listOf("\\", "|", "/", "-")
    while (true) {
        sequence.forEach {
            print(it)
            print('\b')
        }
    }
}
 

To make the printing process slow, we can use the delay() function before omitting the character :

 fun main() {
    runBlocking {
        showLoadingAnimation()
    }
}

suspend fun showLoadingAnimation() {
    val sequence = listOf("\\", "|", "/", "-")
    while (true) {
        sequence.forEach {
            print(it)
            delay(200)
            print('\b')
        }
    }
}
 

This works as expected. Currently loading animation is shown infinitely because while condition is set to true. Next, we have to integrate it with a suspend function task such that only while it executes, loading animation will be shown.

For this, let’s define a generic function executeShowingLoadingAnimation() that takes a single parameter task - a suspend lambda function to be executed showing loading animation.

 suspend fun <T> executeShowingLoadingAnimation(
    task: suspend () -> T
): T {
    var output: T? = null

    return coroutineScope {

        // Execute the task
        launch(Dispatchers.IO) {
            output = task()
        }

        // Show loading animation while output is null
        val sequence = listOf("\\", "|", "/", "-")
        while (output == null) {
            sequence.forEach {
                print(it)
                delay(200)
                print('\b')
            }
        }
        
        // Loop breaks -> output non-null, return it
        output!!
    }
}

// Usage :
fun main() {
    runBlocking {
        val response = executeShowingLoadingAnimation {
            getMathFact(10)
        }

        println("Response = $response")
		}
}
 

Note :

Launching coroutines

Coroutines can not be launched directly from a suspend function. But why launch coroutine from a suspend function? This may be required to perform further parallel executions.

 suspend fun task() {
		launch { // ERROR! Won't work
				// code...
		}
}
 

Recall that launch() function can only be invoked from CoroutineScope, which is not available directly in suspend functions. Suspend functions are executed on a coroutine, so it already has a CoroutineScope. To access the CoroutineScope inside a suspend function, we can use the coroutineScope() function :

 suspend fun <R> coroutineScope(
		lambda: suspend CoroutineScope.() -> R
): R
 

It is a suspend function which takes a suspend lambda as input. Lambda receives CoroutineScope which can be used to launch coroutines.

 suspend fun task() {
		coroutineScope {/* this: CoroutineScope */
				launch { // Works!
						// code...
				}		
		}
}
 

The CoroutineScope received here is of the coroutine in which task() suspend function is being executed.

Lambda returns an output of type R after execution which is then returned by the coroutineScope() function to the calling site. This should be clear from the above LoadingAnimation example.

Not that CoroutineScope class is very different from coroutineScope() function. CoroutineScope class contains coroutine builder functions such as launch(). While coroutineScope() is a function used to access CoroutineScope from within a suspend function.

Non-blocking Network requests

In the network requests example, we observed that our httpGet() function isn’t a suspend function. Due to which it cannot suspend its execution while waiting for the response. Let’s fix this issue using a suspend function for network requests.

Several HTTP Client libraries are available to ease network requests.

HTTP Client refers to a set of classes and functions used for sending HTTP requests as a client.

One such easy to use HTTP Client library is Ktor. It is developed by Jetbrains and written in Kotlin. It makes making network requests easy using Coroutines API.

We’ll now see how to send non-blocking network requests using Ktor HTTP Client Library

Dependencies

To start using Ktor library, add the following Gradle dependencies :

 val ktor_version = "VERSION_HERE"
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-cio:$ktor_version")
implementation("io.ktor:ktor-client-logging:$ktor_version")
implementation("org.slf4j:slf4j-simple:2.0.6")
 
  • client-core library contains the core structure of Ktor HTTP Client.
  • client-cio library provides the engine for Ktor. It contains the functionality that actually processes network requests.
  • client-logging library is for logging the network requests sent by Ktor. It requires an additional library to process the logs. Here, we are using slf4j-simple.
  • slf4j-simple prints the Ktor logs on the console.

Creating HttpClient

To start sending requests, Ktor HTTPClient object is required. It exposes the functions for sending requests. HTTPClient can be intantiated by invoking its constructor and passing the engine (CIO here) :

 val client = HttpClient(CIO)
 

Ktor supports multiple plugins for additional functionalities which can be used by adding their respective Gradle dependencies. Logging is a Ktor plugin which we already added as our Gradle dependency. Plugins can then be installed while creating a client :

 val client = HttpClient(/* Engine */) {
		/* Client Config */

    install(/* Plugin */) {
        /* Plugin Config */
    }
}

// Example :
val client = HttpClient(CIO) {
    install(Logging) {
        level = LogLevel.INFO
    }
}
 

⚠️ Only one client object must be created for an entire application. Creating multiple clients is resource intensive and time consuming.

Sending GET requests

To send GET request, invoke HTTPClient#get() suspend function by passing in the resource url :

 suspend fun HttpClient.get(
    urlString: String,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse
 

Optionally, we can pass in a lambda - block to configure a request.

Return type is HttpResponse which can be used to access response status code, body, request time, response time etc.

To get the response body as String, invoke bodyAsText() function on it :

 val responseBody: String = response.bodyAsText()
 

Example :

 fun main() {
    runBlocking {
        val response = client.get("http://numbersapi.com/3/math").bodyAsText()
        println("MathFact(3) = $response")
    }
}
 

Let’s now verify the non-blocking behaviour of this get() function.

Parallel requests

While using the normal function httpGet(), we had to use multiple threads to send parallel requests. Single thread was insufficient because of blocking behaviour. Now that we are using a suspend function i.e. get() from Ktor Client, coroutine can now suspend while waiting for response. Hence, single thread would suffice :

 val client = HttpClient(CIO)

suspend fun getAndPrintMathFact(num: Int) {
    val response = client.get("http://numbersapi.com/$num/math").bodyAsText()
    println("MathFact($num) = $response")
}

fun main() {
    val parallelTime = measureTimeMillis {
        runBlocking {
            repeat(5) {
                launch {
                    println("sending R#$it from ${Thread.currentThread().name}...")
                    getAndPrintMathFact(it)
                }
            }
        }
    }
    println("\n=> Parallel execution took $parallelTime ms")
}

/* Output : 

sending R#0 from main...
sending R#1 from main...
sending R#2 from main...
sending R#3 from main...
sending R#4 from main...
MathFact(1) = 1 is the first figurate number of every kind, such as triangular number, pentagonal number and centered hexagonal number, to name just a few.
MathFact(0) = 0 is the additive identity.
MathFact(4) = 4 is an all-Harshad number and a semi-meandric number.
MathFact(3) = 3 is the first number, according to the Pythagoreans, and the first male number.
MathFact(2) = 2 is the third Fibonacci number, and the third and fifth Perrin numbers.

=> Parallel execution took 884 ms */
 

Note :

  • A new suspend function getAndPrintMathFact() has been defined, which uses the Ktor get() function internally.
  • Single client object is used throughout.
  • All requests are sent at nearly the same time, that too on the same thread - main. This verfies the fact that coroutine is suspended while waiting for response, allowing for parallel execution of requests.

Sending POST requests

For sending POST requests, post() function is to be used. Request config (header, payload etc.) can be set using the block lambda parameter :

 suspend fun HttpClient.post(
    urlString: String,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse
 

Example :

 fun main() {
		val client = HttpClient(CIO)

    val url = "https://pastebin.com/api/api_post.php"
    val apiKey = "Wd_w0PMNS9z5P-y-54OZnkTx3FS7Iufv"
    val text = "Hello Ktor"

    runBlocking {
        val response = client.post(url) {
            headers {
                append("Content-Type", "application/x-www-form-urlencoded")
            }

            setBody("api_dev_key=$apiKey&api_paste_code=$text&api_option=paste")
        }

        println("Response = ${response.bodyAsText()}")
    }
}
 

Notice the idiomatic way of sending requests using Ktor client library. The code is very concise & self explanatory.