Slide 1

Slide 1 text

Android Http Clients Jussi Pohjolainen

Slide 2

Slide 2 text

Http Clients Feature OkHttp Retrofit Ktor Client Language Java/Kotlin Java/Kotlin 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 No Yes Yes Multiplatform No No 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

Slide 3

Slide 3 text

Year OkHttp Retrofit Ktor 2010 - Initial creation by Square Inc. - 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. Retrofit 2.0 released with significant updates like a new call adapter mechanism. Ktor development begins, leveraging Kotlin's capabilities. 2017 Continuous enhancements and support for HTTP/2. Updates and new features, including better error handling and RxJava support. Ktor officially announced, providing a Kotlin-first approach to web and application development. 2018 - - Momentum grows, with significant updates enhancing functionality. 2019 OkHttp 4.0 released, embracing Kotlin while maintaining Java compatibility. Retrofit adds support for Kotlin coroutines, enhancing its integration with modern development practices. Ktor 1.0 released, marking its maturity for production use. 2020 onwards Ongoing updates, focusing on performance and security enhancements. Continuous improvement, maintaining popularity and adding features to support modern app development. Continuous updates and improvements, expanding support for multiplatform projects.

Slide 4

Slide 4 text

OkHttp

Slide 5

Slide 5 text

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/

Slide 6

Slide 6 text

Dependencies implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")

Slide 7

Slide 7 text

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() }

Slide 8

Slide 8 text

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() } }

Slide 9

Slide 9 text

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}") } }

Slide 10

Slide 10 text

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) }

Slide 11

Slide 11 text

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}") }

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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.

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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!

Slide 16

Slide 16 text

Back to HTTP Fetching fun main() { val startTime = System.currentTimeMillis() runBlocking { repeat(10) { launch { val result = fetchJoke() println(result) } } } val endTime = System.currentTimeMillis() println("Time taken: ${endTime - startTime} ms") }

Slide 17

Slide 17 text

Ktor

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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")

Slide 21

Slide 21 text

CLI app implementation("io.ktor:ktor-client-core:2.3.8") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("io.ktor:ktor-client-content-negotiation:2.3.8") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.8") implementation("io.ktor:ktor-client-cio:2.3.8") implementation("ch.qos.logback:logback-classic:1.5.0")

Slide 22

Slide 22 text

CLI plugin plugins { alias(libs.plugins.jvm) application kotlin("plugin.serialization") version "1.9.22" }

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Usage fun main() = runBlocking { println("START") val joke = fetchAndParse() println(joke.value) println("STOP") }

Slide 27

Slide 27 text

Android app implementation("io.ktor:ktor-client-core:2.3.8") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("io.ktor:ktor-client-content-negotiation:2.3.8") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.8") implementation("io.ktor:ktor-client-android:2.3.8") implementation("ch.qos.logback:logback-classic:1.5.0") kotlin("plugin.serialization") version "1.9.22"

Slide 28

Slide 28 text

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() } }

Slide 29

Slide 29 text

Ktor and Android @Composable fun Ktor() { val scope = rememberCoroutineScope() var joke by remember { mutableStateOf("") } Column { Text( text = joke, ) Button(onClick = { scope.launch { joke = KtorfetchAndParse().value println("${Thread.currentThread().name} ${joke}") } }) { Text("Fetch using ktor") } } }

Slide 30

Slide 30 text

Retrofit

Slide 31

Slide 31 text

Feature Ktor Retrofit Language Kotlin Kotlin/Java Asynchronous Support Native support through Kotlin coroutines Native support with coroutines, RxJava, or callbacks 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

Slide 32

Slide 32 text

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.

Slide 33

Slide 33 text

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")

Slide 34

Slide 34 text

Kotlin Features • Let’s first learn • What is singleton • How do you implement singleton in Java and in Kotlin • What is “by lazy” in Kotlin

Slide 35

Slide 35 text

Singleton • Singleton is a design pattern that restricts the instantiation of a class to one "single" instance. • The Singleton pattern is often used for • managing connections to a database • Logging • file managers • other scenarios where a single point of access to a resource is desirable.

Slide 36

Slide 36 text

public class SingletonExample { private static SingletonExample singleInstance = null; private SingletonExample() {} public static SingletonExample getInstance() { if (singleInstance == null) { singleInstance = new SingletonExample(); } return singleInstance; } public void showMessage() { System.out.println("Hello from the Singleton class!"); } } public class Main { public static void main(String[] args) { SingletonExample singleton = SingletonExample.getInstance(); singleton.showMessage(); } }

Slide 37

Slide 37 text

Kotlin: so much easier! object SingletonExample { // Example method to demonstrate the functionality of the Singleton class. fun showMessage() { println("Hello from the Singleton class!") } } // To use the Singleton class: fun main() { // Get the only object available SingletonExample.showMessage() } Only one object can be created

Slide 38

Slide 38 text

Kotlin: ”static” methods class MathUtils { companion object { fun max(a: Int, b: Int): Int { return if (a > b) a else b } } } fun main() { val maxInt = MathUtils.max(2, 3) println("The maximum of 2 and 3 is $maxInt") } Several objects can be created Companion object basically means static method

Slide 39

Slide 39 text

Lazy class LazyExample { val lazyField: String by lazy { println("Initializing lazyField") "This is a lazily initialized string. ${Math.random()}" } } fun main() { val example = LazyExample() println("Before accessing 'lazyField'") println(example.lazyField) println("After accessing 'lazyField'") println(example.lazyField) } lazy lambda is run Uses the cached version, so same random value

Slide 40

Slide 40 text

Simple Retrofit Example interface ChuckNorrisService { @GET("/jokes/random") suspend fun fetchJoke(): ChuckNorrisJoke } fun main() { println("START on thread: ${Thread.currentThread().name}") runBlocking { val okHttpClient = OkHttpClient.Builder() // Add any other configuration you need .build() val retrofit = Retrofit.Builder() .baseUrl("https://api.chucknorris.io") .addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient) .build() val service = retrofit.create(ChuckNorrisService::class.java) val joke = service.fetchJoke() println(joke.value) okHttpClient.dispatcher().executorService().shutdown(); okHttpClient.connectionPool().evictAll(); } println("STOP on thread: ${Thread.currentThread().name}") }

Slide 41

Slide 41 text

data class ChuckNorrisJoke(val value: String) interface ChuckNorrisService { @GET("/jokes/random") suspend fun fetchJoke(): ChuckNorrisJoke companion object { private val okHttpClient: OkHttpClient by lazy { OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() } val service: ChuckNorrisService by lazy { Retrofit.Builder() .baseUrl("https://api.chucknorris.io") .addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient) .build() .create(ChuckNorrisService::class.java) } fun close() { // no more tasks are accepted okHttpClient.dispatcher().executorService().shutdown() // close connection pool and removes connections okHttpClient.connectionPool().evictAll() } } } fun main() { runBlocking { val service = ChuckNorrisService.service val list : List> = List(10) { async { service.fetchJoke().value } } println(list.awaitAll().joinToString("\n")) ChuckNorrisService.close() } }