$30 off During Our Annual Pro Sale. View Details »

Android HTTP Clients

Jussi Pohjolainen
March 28, 2024
430

Android HTTP Clients

Jussi Pohjolainen

March 28, 2024
Tweet

Transcript

  1. 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
  2. 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.
  3. 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/
  4. 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() }
  5. 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() } }
  6. 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}") } }
  7. 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) }
  8. 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}") }
  9. 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
  10. 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.
  11. 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
  12. 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!
  13. 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") }
  14. 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
  15. 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
  16. 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")
  17. 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
  18. 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
  19. 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
  20. Usage fun main() = runBlocking { println("START") val joke =

    fetchAndParse() println(joke.value) println("STOP") }
  21. 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() } }
  22. 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") } } }
  23. 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
  24. 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.
  25. 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")
  26. 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
  27. 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.
  28. 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(); } }
  29. 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
  30. 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
  31. 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
  32. 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}") }
  33. 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<Deferred<String>> = List(10) { async { service.fetchJoke().value } } println(list.awaitAll().joinToString("\n")) ChuckNorrisService.close() } }