Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin Coroutines

Kotlin Coroutines

Avatar for Jussi Pohjolainen

Jussi Pohjolainen

March 28, 2024

More Decks by Jussi Pohjolainen

Other Decks in Technology

Transcript

  1. Process and Thread • Process • A process is an

    instance of a program running on a computer. • It is a self-contained execution environment and typically has its own memory space. • Processes are independent of each other • Thread • A thread is a smaller sequence of programmed instructions within a process • Threads within the same process share the same memory space but operate independently • Multithreading allows a process to perform multiple tasks concurrently
  2. What is a Thread? • A thread is a single

    sequential flow of execution of tasks of a process in an operating system (OS). • OS threads are at the core of Java’s concurrency model • Java threads are just a wrapper at OS thread • JVM must work with the underlying OS to create and manage that thread, which is quite expensive because the JVM must communicate with the OS back and forth throughout the thread’s lifetime • This switching is an expensive operation, which makes threads expensive
  3. Java public class Main { public static void main(final String[]

    args) { for(int i=0; i<10; i++) { generateThread().start(); } } public static Thread generateThread() { Thread t = new Thread(() -> { for(int i=0; i<10; i++) { System.out.println(Thread.currentThread().getName() + " i = " + i); try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } } }); return t; } }
  4. Thread 1 / CPU Thread 2 / CPU Thread 3

    / CPU Java supports multithreading at OS Level. True parallel execution
  5. Multithreaded environment THREAD 1 THREAD 2 i=1 i=2 i=3 i=4

    i=5 i=6 i=7 i=8 i=1 i=2 i=3 i=4 i=5 i=6 i=7 i=8
  6. JavaScript const sleep = async (msecs) => new Promise((resolve) =>

    { setTimeout(() => { resolve(); }, msecs); }); const count = async () => { for (let i = 0; i <= 10; i++) { console.log(i); await sleep(1000); } }; const main = async () => { count(); count(); }; main();
  7. Kotlin import kotlin.concurrent.thread fun main() { val t1 = Thread({

    println("hello") }) t1.start() val t2 = Thread() { println("hello") } t2.start() Thread { println("hello") }.start() thread { println("hello") } }
  8. Kotlin import kotlin.concurrent.thread fun main() { for(i in 1..10) {

    generateThread().start(); } } fun generateThread() = thread(false) { for(i in 1..10) { println("${Thread.currentThread().name} i = $i") Thread.sleep(1000); } }
  9. JVM Thread 1 | OS Kernel Thread 1 |----------->| JVM

    Thread 2 | OS Kernel Thread 2 |----------->| JVM Thread 3 | OS Kernel Thread 3 |----------->| ... JVM Thread N | OS Kernel Thread N |----------->|
  10. Kotlin coroutine • Kotlin coroutines are entirely managed by the

    language • Do not require communication with the underlying OS to be created and managed • Coroutines are lightweight and can be launched millions at once • ( Java 19 introduced the concept of virtual threads. Virtual threads are lightweight threads that are managed by the JVM rather than the OS )
  11. Kotlin Coroutine • Coroutines are not bound to any particular

    thread • Coroutines can be • Creating async tasks only one thread (like js) • Creating async tasks using thread pool • Dispatchers.IO, Dispatchers.Default • Default thread pool amount is the number of CPU cores • Runtime.getRuntime().availableProcessors()
  12. Two java threads import kotlin.concurrent.thread fun main() { thread {

    repeat(10) { println(it) Thread.sleep(1000) } } thread { repeat(10) { println(it) Thread.sleep(1000) } } } App creates two threads and when threads have finished app ends
  13. Two coroutines fun main() { runBlocking { Runblocking scope is

    for main method or testing, it waits that all coroutines are done and then ends the app launch { repeat(10) { println("A ${Thread.currentThread().name}: $it") delay(1000) } } launch { repeat(10) { println("B ${Thread.currentThread().name}: $it") delay(1000) } } } } Only one thread is used!
  14. Two coroutines fun main() { runBlocking { launch { repeat(10)

    { println("A ${Thread.currentThread().name}: $it") delay(1000) } It is now synchronous } launch { repeat(10) { println("B ${Thread.currentThread().name}: $it") delay(1000) } } } }
  15. Two coroutines fun main() { runBlocking { launch(Dispatchers.Default) { repeat(10)

    { println("A ${Thread.currentThread().name}: $it") delay(1000) } } launch(Dispatchers.Default) { repeat(10) { println("B ${Thread.currentThread().name}: $it") delay(1000) } } } } Dispatchers.Default => CPU intensive work, creates thread pool
  16. Two coroutines fun main() { runBlocking { launch(Dispatchers.Default) { repeat(100)

    { println("A ${Thread.currentThread().name}: $it") delay(1000) } } launch(Dispatchers.Default) { repeat(100) { println("B ${Thread.currentThread().name}: $it") delay(1000) } } } } It will do some multitasking now
  17. Job and cancel fun main() { runBlocking { val job1

    : Job = launch(Dispatchers.Default) { repeat(10) { println("A ${Thread.currentThread().name}: $it") delay(1000) } } job1.cancel() } } It can cancel the coroutine
  18. Job and join import kotlin.concurrent.* import kotlinx.coroutines.* fun main() {

    runBlocking { val jobs: MutableList<Job> = mutableListOf() repeat(10) { jobs.add(launch(Dispatchers.Default) { repeat(10) { println("${Thread.currentThread().name}: $it") delay(200) } }) } // jobs.forEach { it.join() } // Wait for all jobs to complete jobs.joinAll() println("done") } } Waits until the job is done
  19. Job and cancel fun main() { runBlocking { val job1

    : Job = launch(Dispatchers.Default) { repeat(10) { println("A ${Thread.currentThread().name}: $it") delay(200) } } job1.join() println("Stop") } } Waits until the job is done
  20. async fun main() { runBlocking { val coroutine: Deferred<Int> =

    async(Dispatchers.Default) { var sum = 0 repeat(10) { sum += it delay(100) Async will return a value! } sum } val result = coroutine.await() println(result) } } Will wait until it receives the result
  21. async fun main() { runBlocking { val coroutines : MutableList<Deferred<Int>>

    = mutableListOf() repeat(100) { coroutines.add(async(Dispatchers.Default) { var sum = 0 println(Thread.currentThread().name) repeat(10) { sum += it delay(100) } sum }) } } } Uses thread pool val results : List<Int> = coroutines.awaitAll() println(results) Will wait until it receives the result
  22. Callbacks fun main() { println("a") readFile("src/main/kotlin/org/example/App.kt") { err, data ->

    // Runs later, on the background thread if (err != null) { println("Callback error: ${err.message}") } else { println("Callback result: $data") } } println("b") }
  23. Callbacks fun readFile(path: String, callback: (Throwable?, String?) -> Unit): Thread

    { val thread = Thread { try { Thread.sleep(1000) // Simulate a slow disk so timings are visible val data = Files.readString(Path.of(path)); callback(null, data) // Success: no error, data present } catch (e: Exception) { callback(e, null) // Failure: error present, no data } } thread.start() return thread // Returned so callers can interrupt it later }
  24. In JS you can use Promises const fs = require("fs");

    function read(path) { return new Promise((resolve, reject) => { fs.readFile(path, "utf8", (error, data) => { if (error) { reject(error); } else { resolve(data); } }); }); } async function main() { try { const content = await read("src/main/kotlin/org/example/App.kt"); console.log(content); } catch (e) { console.log(`Failed: ${e.message}`); } } async and await
  25. fun log(message: String) { println("[${Thread.currentThread().name}] $message") } suspend fun readFileAsync(path:

    String): String { log("readFileAsync: start") val result = suspendCancellableCoroutine { cont -> val thread = readFile(path) { err, data -> if (err != null) { log("callback: calling resumeWithException") cont.resumeWithException(err) // Like reject(err) } else { log("callback: calling resume") cont.resume(data!!) // Like resolve(data) } } // If the waiting coroutine is cancelled, stop the background thread cont.invokeOnCancellation { thread.interrupt() } log("readFileAsync: lambda done, suspending now") } log("readFileAsync: resumed, returning ${result.length} chars") return result } fun main() = runBlocking { try { // No await needed: the call suspends until the callback resumes it val data = readFileAsync("src/main/kotlin/org/example/App.kt") println("Suspend result: $data") } catch (e: IOException) { // resumeWithException is rethrown here, like .catch() in JS. println("Suspend error: ${e.message}") } }
  26. async fun main() = runBlocking { val time1 = measureTimeMillis

    { val data1 = readFileAsync("src/main/kotlin/org/example/App.kt") val data2 = readFileAsync("src/main/kotlin/org/example/App.kt") } log("$time1") // 2 sec val time2 = measureTimeMillis { val deferredObject1: Deferred<String> = async { readFileAsync("src/main/kotlin/org/example/App.kt") } val deferredObject2: Deferred<String> = async { readFileAsync("src/main/kotlin/org/example/App.kt") } deferredObject1.await() deferredObject2.await() } log("$time2") // 1 sec }
  27. "Promise.all" fun main() = runBlocking { val path1 = "src/main/kotlin/org/example/App.kt"

    val path2 = "src/main/kotlin/org/example/App.kt" val files = listOf(path1, path2) val deferredObjects: List<Deferred<String>> = files.map { path -> val deferredObject: Deferred<String> = async { readFileAsync(path) } deferredObject } val contents : List<String> = deferredObjects.awaitAll() print(contents) }
  28. Cleaned fun main() = runBlocking { val path1 = "src/main/kotlin/org/example/App.kt"

    val path2 = "src/main/kotlin/org/example/App.kt" val list: List<String> = listOf(path1, path2).map { async { readFileAsync(it) } }.awaitAll() print(list) }
  29. thread solution is not a good one suspend fun readFileIO(path:

    String): String { val content = withContext(Dispatchers.IO) { Thread.sleep(1000) // Simulate a slow disk; blocking is fine on IO Files.readString(Path.of(path)) } return content } fun main() = runBlocking { val path = "src/main/kotlin/org/example/App.kt" try { val result = readFileIO(path) println(result) } catch (err: IOException) { println(err) } }
  30. async and launch fun main() = runBlocking { val path

    = "src/main/kotlin/org/example/App.kt" repeat(10) { launch { try { print(readFileIO(path)) } catch (err: IOException) { println(err) } } } val results: List<String?> = List(10) { async { try { readFileIO(path) } catch (err: IOException) { println(err) null } } }.awaitAll() }
  31. JS vs Kotlin JS Kotlin Mark a function async function

    suspend fun Return type Promise<String> String Waiting await just call the function Future value object Promise Deferred
  32. suspend fun fetchRandomJoke(): String { val client = HttpClient.newHttpClient() val

    request = HttpRequest.newBuilder() .uri(URI.create("https://api.chucknorris.io/jokes/random")) .GET() .build() Changes the context to thread pool! May wait if thread pool is full return withContext(Dispatchers.IO) { println(Thread.currentThread().name) val response = client.send(request, HttpResponse.BodyHandlers.ofString()) if(response.statusCode() != 200) { throw RuntimeException("Failed to fetch joke, status code: ${response.statusCode()}") } response.body() } } fun main() { runBlocking { println(Thread.currentThread().name) val deferredList = List(10) { async { fetchRandomJoke() } } val jokes = deferredList.awaitAll() println(jokes) } } main thread Do asynchro nously 10 fetching
  33. Network Connection Feature/Aspect Ktor OkHttp Retrofit Java's HttpURLConnection Language/Platform Kotlin

    Java/Kotlin Java/Kotlin Java Coroutine Support Native support Coroutine wrappers available Native support with Coroutine Adapter Coroutine wrappers needed Asynchronous Model Built-in coroutines Callbacks/Futures Callbacks/Futures/Coroutines with Adapter Synchronous/blocking (Async with Threads) HTTP/2 Support Yes Yes Yes (via OkHttp) Yes (Java 9+) WebSocket Support Yes Yes No (Uses OkHttp) No Multiplatform Yes (JVM, JS, Native) JVM JVM JVM Serialization Built-in formats support No built-in support Converter libraries (e.g., Gson, Moshi) Manual or use third-party libraries Configuration/Usage DSL for configuration Builder pattern Interface annotations URLConnection API Community/Support Growing Large Large Ubiquitous Use Case Full-stack (client/server) Client HTTP requests Type-safe HTTP client Basic HTTP requests
  34. Ktor • Developed by JetBrains • Gives you coroutine friendly

    API that can use different engines • Ktor uses Kotlin coroutines to handle concurrency • Ktor supports JVM, Android, JavaScript, and Native platforms • Ktor also includes a multiplatform HTTP client library that supports making asynchronous requests, streaming, and websockets, fully integrated with Kotlin coroutines.
  35. // one shared client, no need to create multiple //

    You can use different http clients, CIO is Ktor's own val client = HttpClient(CIO) suspend fun apiRequest(url: String): String { // client.get suspends instead of blocking, so it is safe to call // from any thread and many requests can run concurrently val response: HttpResponse = client.get(url) return response.bodyAsText() } fun main() { try { runBlocking { repeat(10) { launch { val data = apiRequest("https://api.chucknorris.io/jokes/random") println(data) } } } } finally { client.close() } }
  36. fun main() { client.use { runBlocking { repeat(10) { launch

    { val data = apiRequest("https://api.chucknorris.io/jokes/random") println(data) } } } } }
  37. Parsing JSON Feature/Aspect Kotlinx.serialization Moshi Gson Jackson Coroutine Support Native

    support Coroutine support with adapters Not specifically designed for coroutines, but can be used with them Coroutine support with adapters Kotlin Language Support Full support (Kotlin-first) Excellent with Kotlin extensions Good, but not Kotlin-first Good with Kotlin module Multiplatform Support Yes (JVM, JS, Native) No (JVM and Android-focused) No (JVM focused) No (JVM focused) Reflection Uses Kotlin's own serialization Uses reflection or codegen Primarily uses reflection Uses reflection or codegen Annotation Processing/Codegen Optional Required for optimal performance Not required Required for optimal performance Default Value Support Excellent Excellent with codegen Limited Excellent with Kotlin module Null Safety Fully supported Fully supported with codegen Limited Fully supported with Kotlin module Performance Very good Very good with codegen Good Very good with codegen Maven Central Artifact Yes Yes Yes Yes JSON to/from Data Class Yes Yes Yes Yes Project Maturity Mature, officially supported by JetBrains Mature, widely used in Android Very mature, widely used Very mature, widely used
  38. kotlinx.serialization val client = HttpClient(CIO) val json = Json {

    ignoreUnknownKeys = true } // Create a Json instance with configuration @Serializable data class ChuckNorrisJoke( val value: String ) suspend fun fetchData(url: String): String { val response: HttpResponse = client.get(url) return response.bodyAsText() } fun parseJoke(jsonString: String): ChuckNorrisJoke { // ChuckNorrisJoke.serializer() => KSerializer<ChuckNorrisJoke>: an object that // knows how to convert a ChuckNorrisJoke to and from JSON (or another format). return json.decodeFromString(ChuckNorrisJoke.serializer(), jsonString) } fun main() { runBlocking { launch { val data = fetchData("https://api.chucknorris.io/jokes/random") val obj: ChuckNorrisJoke = parseJoke(data) println(obj.value) } } }
  39. If JSON is large suspend fun parseJoke(jsonString: String): ChuckNorrisJoke {

    return withContext(Dispatchers.Default) { json.decodeFromString(ChuckNorrisJoke.serializer(), jsonString) } }
  40. Cleaned version @Serializable data class ChuckNorrisJoke( val value: String )

    // One shared client for the whole program val client = HttpClient(CIO) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } suspend fun fetchAndParse(url: String): ChuckNorrisJoke = client.get(url).body() fun main() { runBlocking { repeat(10) { launch { val obj: ChuckNorrisJoke = fetchAndParse("https://api.chucknorris.io/jokes/random") println(obj.value) } } } }
  41. @Serializable data class ChuckNorrisJoke( val value: String ) suspend fun

    fetchAndParse(url: String): ChuckNorrisJoke { val client = HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } return client.get(url).body() } fun main() { val oneThreadTime = measureTimeMillis { runBlocking { repeat(10) { launch { val obj: ChuckNorrisJoke = fetchAndParse("https://api.chucknorris.io/jokes/random") println("(${Thread.currentThread().name}): ${obj.value}") } } } } println("**************") val threadPoolTime = measureTimeMillis { runBlocking { repeat(10) { launch(Dispatchers.Default) { val obj: ChuckNorrisJoke = fetchAndParse("https://api.chucknorris.io/jokes/random") println("(${Thread.currentThread().name}): ${obj.value}") } } } } println("**************") val threadPoolTimeWithIO = measureTimeMillis { runBlocking { repeat(10) { launch(Dispatchers.IO) { val obj: ChuckNorrisJoke = fetchAndParse("https://api.chucknorris.io/jokes/random") println("(${Thread.currentThread().name}): ${obj.value}") } } } } println("One thread $oneThreadTime") println("Dispatchers.Default: $threadPoolTime") println("Dispatchers.IO: $threadPoolTimeWithIO") }
  42. Exception Handling suspend fun fetchAndParse(url: String): ChuckNorrisJoke { val client

    = HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } try { return client.get(url).body(); } catch (e: Exception) { println("An error occurred: ${e.message}") throw e } finally { client.close() } } You should always close the httpclient
  43. Exception Handling: use suspend fun fetchAndParse(url: String): ChuckNorrisJoke { val

    client = HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } } client.use { try { return it.get(url).body() } catch (e: Exception) { println("An error occurred: ${e.message}") throw e } } Java’s try with resources
  44. @Composable fun App() { var i = 1 Column {

    Text( text = "$i", ) Row { Button(onClick = { i++ }) { Text("+") } Button(onClick = { i-}) { Text("-") } } } }
  45. @Composable fun App() { val i : MutableState<Int> = remember

    { mutableIntStateOf(0) } Column { Text( text = "${i.value}", ) Row { Button(onClick = { i.value++ }) { Text("+") } Button(onClick = { i.value-}) { Text("-") } } } }
  46. @Composable fun App() { var i by remember { mutableIntStateOf(0)

    } Column { Text( text = "${i}", ) Row { Button(onClick = { i++ }) { Text("+") } Button(onClick = { i-}) { Text("-") } } } } Invokes the setter and getter of value by default
  47. @Composable fun App() { // Remembering a value across recompositions

    var count by remember { mutableIntStateOf(0) } // Remembering a coroutine scope across recompositions val coroutineScope = rememberCoroutineScope() Column { Button(onClick = { coroutineScope.launch { repeat(10) { // Perform asynchronous operation delay(1000) // Update the count count++ } } }) { Text(text = "Click me") } // Display the count Text(text = "Count: ${count}") } }
  48. @Composable fun App() { var toggle by remember { mutableStateOf(true)

    } Column { Button(onClick = { toggle = !toggle }) { Text("toggle") } if(toggle) { ButtonStuff() } } } @Composable fun ButtonStuff() { val scope = rememberCoroutineScope() Column { Button(onClick = { scope.launch { repeat(10) { println(it) delay(1000) } } }) { Text("Start") } } }
  49. LaunchedEffect • LaunchedEffect is used to trigger side effects that

    should only be executed once when the key (a specified dependency) changes. It runs the side effect code in a coroutine scope, allowing asynchronous work without blocking the UI. • This is useful for operations like making a network request, performing disk I/O, or any task that should be triggered when a specific value or dependency changes. • The side effect is launched in a coroutine and is independent of the Composable function’s recomposition.
  50. DisposableEffect • DisposableEffect is used to manage resources that need

    to be acquired and released when a Composable is created and destroyed (disposed). • The effect code inside DisposableEffect runs when the Composable is first recomposed, and when the key (dependency) changes, it will dispose of the previous resource before running the effect code again. • This is useful for tasks like acquiring and releasing resources, such as registering and unregistering observers or listeners. • The side effect is executed every time the Composable is recomposed and is subject to recomposition.
  51. @Composable fun App() { var checkedState by remember { mutableStateOf(true)

    } Column { Button(onClick = { checkedState = !checkedState }) { Text("toggle: $checkedState") } if(checkedState) { Clock() } } Invokes the setter and getter of value by default } @Composable fun Clock() { // Remember the current time state var time by remember { mutableStateOf(currentTime()) } // Use LaunchedEffect with Unit to start the effect immediately and only once LaunchedEffect(Unit) { while (true) { time = currentTime() // Update time every second println(time) delay(1000) // Wait for one second } } // Display the current time Text(text = time) } // Helper function to get the current time as a formatted String fun currentTime(): String = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) Will leave it running, you could use DisposableEffect
  52. @Serializable data class StarWarsCharacter(val name: String) @Composable fun StarWarsPerson(id: Int)

    { var name by remember { mutableStateOf("Loading...") } LaunchedEffect(id) { val client = HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } try { val character: StarWarsCharacter = client.get("https://swapi.dev/api/people/$id/").body() name = character.name } catch (e: Exception) { e.printStackTrace() name = "Failed to load data" } finally { client.close() } } Text(text = name) }
  53. Typical Scenario • Connect to Backend using http clients •

    Many available • Handle threading and permissions • Handle JSON parsing • Display content in UI
  54. Http Clients Feature Language OkHttp Java/Kotlin Retrofit Java/Kotlin Ktor Client

    Kotlin Base N/A OkHttp Ktor framework HTTP/2 Support Yes Yes (through OkHttp) Yes SPDY Support Yes Yes (through OkHttp) Yes Coroutine Support Kotlin coroutines compatible Kotlin coroutines compatible Yes Synchronous & Asynchronous Yes Yes Yes Type-Safety Multiplatform No No Yes No Yes Yes Connection Pooling Yes Yes (through OkHttp) Yes GZIP Support Yes Yes (through OkHttp) Yes Caching Yes Yes (through OkHttp) Yes Ease of Use Moderate High Moderate
  55. Year 2010 OkHttp - Retrofit Initial creation by Square Inc.

    Ktor - 2011 - - Kotlin is unveiled by JetBrains. 2012 OkHttp 1.0 is released by Square Inc. - 2013 - Retrofit 1.0 released, offering a REST client for Android and Java. 2014 OkHttp 2.0 released, introducing major updates and improvements. - - 2016 OkHttp 3.0 released, further improvements and features. 2017 Continuous enhancements and support for HTTP/2. 2018 - 2019 OkHttp 4.0 released, embracing Kotlin while maintaining Java compatibility. 2020 onwards Ongoing updates, focusing on performance and security enhancements. Retrofit 2.0 released with significant updates like a new call adapter mechanism. Updates and new features, including better error handling and RxJava support. - - Ktor development begins, leveraging Kotlin's capabilities. Ktor officially announced, providing a Kotlin-first approach to web and application development. Momentum grows, with significant updates enhancing functionality. Retrofit adds support for Kotlin Ktor 1.0 released, marking its maturity coroutines, enhancing its integration for production use. with modern development practices. Continuous improvement, maintaining Continuous updates and popularity and adding features to improvements, expanding support for support modern app development. multiplatform projects.
  56. OkHttp (Square inc) • Designed for use on Android, but

    suitable for JVM • First release in 2013, idea was to create a better than the official HttpURLConnection • Features • Efficiency, Http/2, Connection pooling • Integration with Retrofit (very popular tool, coming later on) • 2019: 100% Kotlin support • https://square.github.io/okhttp/
  57. Sync Simple Version fun main() { val client = OkHttpClient()

    val request = Request.Builder() .url("https://api.chucknorris.io/jokes/random") .build() val response: Response = client.newCall(request).execute() val result = response.body?.string() ?: "error" println(result) response.close() }
  58. Error Handling fun main() { val client = OkHttpClient() val

    request = Request.Builder() .url("https://api.chucknorris.io/jokes/random2") .build() var response: Response? = null try { response = client.newCall(request).execute() val result = response.body?.string() ?: "Failed to fetch the joke - no response body" println(result) } catch (e: Exception) { println("An error occurred: ${e.message}") } finally { response?.body?.close() } }
  59. use -> automatic closing fun main() { val client =

    OkHttpClient() val request = Request.Builder() .url("https://api.chucknorris.io/jokes/random") .build() try { client.newCall(request).execute().use { resp -> val result = resp.body?.string() ?: "Failed to fetch the joke - no response body" println(result) } } catch (e: Exception) { println("An error occurred: ${e.message}") } }
  60. Sync function fun fetchJoke() : String { val client =

    OkHttpClient() val request = Request.Builder() .url("https://api.chucknorris.io/jokes/random") .build() client.newCall(request).execute().use { response -> return response.body?.string() ?: "Failed to fetch the joke - no response body" } } fun main() { val json = fetchJoke() println(json) }
  61. Async function fun fetchJoke(callback: (result: String) -> Unit) { val

    client = OkHttpClient() val request = Request.Builder() .url("https://api.chucknorris.io/jokes/random") .build() thread { client.newCall(request).execute().use { response -> callback(response.body?.string() ?: "Failed to fetch the joke - no response body") } } } fun main() { println("START on thread: ${Thread.currentThread().name}") fetchJoke { println("Joke fetched on thread: ${Thread.currentThread().name} - $it") // you could do another fetchJoke here -> callback hell } println("STOP on thread: ${Thread.currentThread().name}") }
  62. Disadvantages: thread + callback • Manual thread management • No

    thread pool to reuse • Handling errors in callbacks can be hard • Callback hell • Complex lifecycle management • If using with android, you cannot manage UI with worker thread
  63. Coroutine example 1 fun main() { runBlocking { // starts

    async fetch val joke1 : Deferred<String> = async(Dispatchers.IO) { fetchJoke() } // starts another async fetch val joke2 : Deferred<String> = async(Dispatchers.IO) { fetchJoke() } // Parallel fetching happening, will be waiting that both are ready println(joke1.await()) println(joke2.await()) } }
  64. Coroutine example 2 fun main() { // Let's ensure that

    app ends after last coroutine exists runBlocking { val startTime = System.currentTimeMillis() val joke1 : Deferred<String> = async(Dispatchers.IO) { delay(2000) "hello" } val joke2 : Deferred<String> = async(Dispatchers.IO) { delay(3000) "world" } // both will run parallel, until the last on exists we are ready to go. println(joke1.await()) println(joke2.await()) } } val endTime = System.currentTimeMillis() println("Time taken: ${endTime - startTime} ms")
  65. Coroutine example 3 fun main() = runBlocking { val jokes

    = mutableListOf<Deferred<String>>() // Launch 10 asynchronous fetches repeat(10) { val jokeDeferred: Deferred<String> = async(Dispatchers.IO) { fetchJoke() } jokes.add(jokeDeferred) } // Wait for all the jokes to be fetched and print them jokes.forEach { deferredJoke -> println(deferredJoke.await()) } }
  66. Coroutine example 4 suspend fun fetchJoke(): String { delay(1000) return

    "Hello" } fun main() { runBlocking { // starts async fetch val joke1 : Deferred<String> = async(Dispatchers.IO) { fetchJoke() } // starts another async fetch val joke2 : Deferred<String> = async(Dispatchers.IO) { fetchJoke() } } } // Parallel fetching happening, will be waiting that both are ready println(joke1.await()) println(joke2.await()) delay is a suspend function which can be called only within a suspend funcion This is rather ugly way of getting the result “Hello”
  67. Coroutine example 5 suspend fun fetchJoke(): String { delay(1000) return

    "Hello" } Delay allows other coroutines do there job fun main() { val startTime = System.currentTimeMillis() runBlocking { launch { val joke = fetchJoke() println(joke) } launch { val joke = fetchJoke() println(joke) } } } Runs parallel, notice the easy return value, only one thread is used! val endTime = System.currentTimeMillis() println("Time taken: ${endTime - startTime} ms") ~1 sec
  68. Coroutine example 5 fun calculateStuff(): String { val startTime =

    System.currentTimeMillis() var value = -1 while(value != 5) { value = (1..1000000000).random() } val endTime = System.currentTimeMillis() println("Calc Time taken: ${endTime - startTime} ms") } return "result" fun main() { val startTime = System.currentTimeMillis() runBlocking { repeat(10) { launch { val result = calculateStuff() println(result) } } } } val endTime = System.currentTimeMillis() println("Time taken: ${endTime - startTime} ms") Because one thread is used, this is sync!
  69. Coroutine example 6 fun calculateStuff(): String { val startTime =

    System.currentTimeMillis() var value = -1 while(value != 5) { value = (1..1000000000).random() } val endTime = System.currentTimeMillis() println("Calc Time taken: ${endTime - startTime} ms") } return "result" fun main() { val startTime = System.currentTimeMillis() runBlocking { repeat(10) { launch(Dispatchers.Default) { val result = calculateStuff() println(result) } } } } val endTime = System.currentTimeMillis() println("Time taken: ${endTime - startTime} ms") Now it is parallel
  70. Back to HTTP Fetching suspend fun fetchJoke(): String { val

    client = OkHttpClient() val request = Request.Builder() .url("https://api.chucknorris.io/jokes/random") .build() val value = withContext(Dispatchers.IO) { client.newCall(request).execute().use { response -> println("${Thread.currentThread().name}") return@withContext response.body?.string() ?: "Failed to fetch the joke - no response body" } } return value } This part will be done in IO thread pool.
  71. Back to HTTP Fetching suspend fun fetchJoke(): String { val

    client = OkHttpClient() val request = Request.Builder() .url("https://api.chucknorris.io/jokes/random") .build() return withContext(Dispatchers.IO) { client.newCall(request).execute().use { response -> println("${Thread.currentThread().name}") return@withContext response.body?.string() ?: "Failed to fetch the joke - no response body" } } } Return directly
  72. Back to HTTP Fetching suspend fun fetchJoke(): String { val

    client = OkHttpClient() val request = Request.Builder() .url("https://api.chucknorris.io/jokes/random") .build() return withContext(Dispatchers.IO) { client.newCall(request).execute().use { response -> response.body?.string() ?: "Failed to fetch the joke - no response body" } } } If no return, it will be automatically return for the outer lambda!
  73. Back to HTTP Fetching fun main() { val startTime =

    System.currentTimeMillis() runBlocking { repeat(10) { launch(Dispatchers.Default) { val result = fetchJoke() println(result) } } } val endTime = System.currentTimeMillis() println("Time taken: ${endTime - startTime} ms") }
  74. Feature OkHttp Ktor Language Kotlin/Java Kotlin Platform Android, Java VM

    Multiplatform (JVM, iOS, JS) Asynchronous Yes, with Call.enqueue Yes, Coroutine based HTTP/2 Support Yes Yes WebSockets Yes Yes Multiplatform No, JVM-based only Yes Streaming Yes Yes Interceptors Yes, request and response Yes, features mechanism Testing MockWebServer for testing Built-in testing framework Community/Support Large, mature Growing, Kotlin-focused Setup Complexity Simple integration Requires coroutine knowledge
  75. Ktor • Kotlin framework for asynchronous servers and clients •

    2019: Version 1.0 • Suitable for web applications, HTTP services, mobile, and browser apps • Uses Kotlin's coroutines for non-blocking I/O • Customizable through features and plugins
  76. Dependencies for client • Basic coroutines • implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") • Core

    Library for Ktor Client • implementation("io.ktor:ktor-client-core:2.3.8") • Engine for android • implementation("io.ktor:ktor-client-android:2.3.8") • Engine for CLI • implementation("io.ktor:ktor-client-cio:2.3.8") • Automatic serialization plugin from content-type • implementation("io.ktor:ktor-client-content-negotiation:2.3.8") • Bridge between ktor -> kotlinx.serialization • implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.8") • JSON Serialization • implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
  77. Code @Serializable data class ChuckNorrisJoke(val value: String) suspend fun fetchAndParse():

    ChuckNorrisJoke { val client = HttpClient(CIO) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } try { return client.get("https://api.chucknorris.io/jokes/random").body(); } catch (e: Exception) { println("An error occurred: ${e.message}") throw e } finally { client.close() } } You should always close the httpclient
  78. Exception Handling: use suspend fun fetchAndParse(): ChuckNorrisJoke { val client

    = HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } } client.use { try { return it.get("https://api.chucknorris.io/jokes/random").body() } catch (e: Exception) { println("An error occurred: ${e.message}") throw e } } Java’s try with resources
  79. Exception Handling: use suspend fun fetchAndParse(url: String): ChuckNorrisJoke { val

    client = HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } client.use { return it.get("https://api.chucknorris.io/jokes/random").body() } } You can throw the error directly to console if you want
  80. Usage Creates a new coroutine, uses IO thread pool fun

    main() = runBlocking { println("START") launch(Dispatchers.IO) { val joke = fetchAndParse() println(joke.value) } println("STOP") }
  81. Usage fun main() = runBlocking { println("START on thread: ${Thread.currentThread().name}")

    repeat(10) { launch(Dispatchers.IO) { val joke = fetchAndParse() println("${Thread.currentThread().name} ${joke.value}") } } println("STOP on thread: ${Thread.currentThread().name}") }
  82. Ktor and Android @Serializable data class ChuckNorrisJoke(val value: String) suspend

    fun KtorfetchAndParse(): ChuckNorrisJoke { val client = HttpClient(Android) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } client.use { return it.get("https://api.chucknorris.io/jokes/random").body() } }
  83. Ktor and Android @Composable fun Ktor() { val scope =

    rememberCoroutineScope() var joke by remember { mutableStateOf("") } Column { Text( text = joke, ) Button(onClick = { scope.launch(Dispatchers.IO) { joke = KtorfetchAndParse().value println("${Thread.currentThread().name} ${joke}") } }) { Text("Fetch using ktor") } } }
  84. Feature Language Ktor Kotlin Retrofit Kotlin/Java Native support with coroutines,

    RxJava, or callbacks Asynchronous Support Native support through Kotlin coroutines HTTP Engine Multiplatform (CIO, OkHttp, Jetty, etc.) OkHttp Serialization Flexible, supports various formats through plugins Gson, Moshi, Jackson, or converters Multiplatform Yes (JVM, Android, JavaScript, Native, iOS) No (Focused on JVM and Android) HTTP 2 Support Yes Yes Websockets Support Yes Not natively, available through OkHttp Client/Server Both (Can be used to create client and server) Client only DSL for Configuration Yes, has a type-safe DSL for building HTTP clients No, uses annotations and interfaces Community and Support Growing, backed by JetBrains Well-established, large community Customizability Highly customizable and extensible Customizable with interceptors and converters Learning Curve Moderate, requires understanding of coroutines Easy to moderate, especially for Retrofit users
  85. Retrofit • High-level abstraction: • Simplifies the process of consuming

    RESTful web services. It automatically handles request and response serialization and deserialization with minimal boilerplate code. • Type-safe HTTP client: • Allows for defining API interfaces in code, making API calls more intuitive and reducing the risk of errors. • Designed specifically for Android and Java: • Though it can be used in any Java application, its features and community support are particularly strong in the Android ecosystem. • Integration with OkHttp: • It uses OkHttp for the underlying HTTP communication, benefiting from OkHttp’s powerful features like connection pooling, GZIP compression, and caching.
  86. Dependencies: Retrofit And Parser • Retrofit • implementation("com.squareup.retrofit2:retrofit:2.9.0") • Parser:

    • GSON, Jackson, Moshi, ProtoBuf, Wir, Simple XML, JAXB • For example Gson: • implementation("com.squareup.retrofit2:converter-gson:2.9.0")
  87. Code // Define your data model data class ChuckNorrisJoke(val value:

    String) // Define the Retrofit service interface interface ChuckNorrisService { @GET("/jokes/random") suspend fun fetchJoke(): Response<ChuckNorrisJoke> } fun main() { runBlocking { val retrofit = Retrofit.Builder() .baseUrl("https://api.chucknorris.io") .addConverterFactory(GsonConverterFactory.create()) .build() val service = retrofit.create(ChuckNorrisService::class.java) } } val job = launch(Dispatchers.IO) { val response = service.fetchJoke() val joke : ChuckNorrisJoke? = response.body() if(joke != null) { println(joke.value) } } job.join() System.exit(0)
  88. // Define your data model data class ChuckNorrisJoke(val value: String)

    // Define the Retrofit service interface interface GhuckNorrisService { @GET("/jokes/random") suspend fun fetchJoke(): Response<ChuckNorrisJoke> companion object { fun getService(): ChuckNorrisService { val retrofit = Retrofit.Builder() .baseUrl("https://api.chucknorris.io") .addConverterFactory(GsonConverterFactory.create()) .build() } } } return retrofit.create(ChuckNorrisService::class.java) fun main() { runBlocking { val service = GitHubService.getService() } } repeat(10) { launch(Dispatchers.IO) { val response = service.fetchJoke() val joke : ChuckNorrisJoke? = response.body() if(joke != null) { println(joke.value) } } }