Http Request with Ktor-client, Jetpack Compose Android project example

Http Request with Ktor-client, Jetpack Compose Android project example

Introduction

Advantages of using Ktor Client:

  1. Cross-platform support: Unlike Retrofit, which is Java-based and has different implementations for Android and iOS, Ktor is built on Kotlin Multiplatform Mobile (KMM). This means you can create both iOS and Android applications with Kotlin and share a significant portion of Kotlin code for both platforms. Ktor is designed for various platforms, such as Android, Native (iOS and desktop), JVM, and JavaScript.

  2. Asynchronous and Coroutine support: Ktor is an asynchronous HTTP client that is entirely built on coroutines, enabling asynchronous programming with minimal boilerplate code.

  3. Flexible and customizable: Ktor provides a flexible and easy-to-use API for making HTTP requests. You can easily customize requests by adding headers, query parameters, or other options.

  4. Serialization and Logging support: Ktor integrates with kotlinx.serialization for serializing and deserializing JSON data, and it has a logging feature that logs all requests and responses, which helps in debugging the application.

Setup new Compose Project

After creating the project, open AndroidManifest.xml and add internet permission as shown below:

<uses-permission android:name="android.permission.INTERNET"/>

Internet permission is required to make HTTP requests to the API.

Add dependency

The main client functionality is available in the ktor-client-core artifact.

implementation "io.ktor:ktor-client-core:$ktor_version"

We need to use ContentNegotiation for Serializing/deserializing sending request and receiving response JSON. To use ContentNegotiation, we need to include the ktor-client-content-negotiation artifact in the build script:

implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"

To serialize/deserialize JSON data, you can choose one of the following libraries: kotlinx.serialization

implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"

Logging

implementation "io.ktor:ktor-client-logging:$ktor_version"

Android engine targets Android and can be configured in the following way:

implementation "io.ktor:ktor-client-android:$ktor_version"

Finally look like this:

def ktor_version = '2.3.0'
//Ktor Dependencies
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
implementation "io.ktor:ktor-client-android:$ktor_version"
implementation "io.ktor:ktor-client-logging:$ktor_version"

You can replace $ktor_version with the required Ktor version, for example, 2.3.0.

In project build.gradle(Module :app) inside plugin add

plugins {  
  /*
  other plugins...
  */

  id("org.jetbrains.kotlin.plugin.serialization") version "1.8.21"  
  }

otherwise it gives you io.ktor.serialization.JsonConvertException: Illegal input Error.

or Add in the gradle.app the next line in the plugins block : id 'kotlinx-serialization'

plugins {
    id 'kotlinx-serialization'
}

Get the Data from API endpoints

For Sending and Receiving we need to create a data class Create a Models package. Create a new Kotlin data class inside this package and name it Post.kt to receive and send data, you need to have a data class, for example:

data class Post (
    val id:Int,
    val title:String,
    val body: String,
    val userId:String 
)

Make sure that this class has the @Serializable annotation, otherwise it throws kotlinx.serialization error:

@Serializable
data class Post (
   val id:Int,
   val title:String,
   val body: String,
   val userId:String 
)

Now, Setup our API routes refer from JSONPlaceholder - Guide but we using only these 4 routes:

  1. POST: https://jsonplaceholder.typicode.com/posts

  2. PUT: https://jsonplaceholder.typicode.com/posts/1

  3. DELETE: https://jsonplaceholder.typicode.com/posts/1

  4. POST: https://jsonplaceholder.typicode.com/posts

Create a new package Network for all our API calls. And we’ll create a new Kotlin Object file named ApiRoutes.kt.

object ApiRoutes {  
  private const val BASE_URL:String = "https://jsonplaceholder.typicode.com"  
  var BLOG_POST = "$BASE_URL/posts"  
}

After creating ApiRoutes , We create a new Kotlin object name as ApiClient.kt for our HttpClient and a variable client.

object ApiClient {  
  //body 
  }

and let's install all required plugins. Final, object looks like,

package com.rhytham.ktorclient.Network  

import io.ktor.client.HttpClient  
import io.ktor.client.engine.android.Android  
import io.ktor.client.plugins.DefaultRequest  
import io.ktor.client.plugins.HttpTimeout  
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation  
import io.ktor.client.plugins.logging.LogLevel  
import io.ktor.client.plugins.logging.Logging  
import io.ktor.client.request.accept  
import io.ktor.client.request.header  
import io.ktor.http.ContentType  
import io.ktor.http.HttpHeaders  
import io.ktor.serialization.kotlinx.json.json  
import kotlinx.serialization.ExperimentalSerializationApi  
import kotlinx.serialization.json.Json  

object ApiClient {  

  //Configure the HttpCLient  
  @OptIn(ExperimentalSerializationApi::class)  
  var client = HttpClient(Android) {  

  // For Logging  
  install(Logging) {  
  level = LogLevel.ALL  
  }  

  // Timeout plugin  
  install(HttpTimeout) {  
  requestTimeoutMillis = 15000L  
  connectTimeoutMillis = 15000L  
  socketTimeoutMillis = 15000L  
  }  

  // JSON Response properties  
  install(ContentNegotiation) {  
  json(  
  Json {  
  ignoreUnknownKeys = true  
  prettyPrint = true  
  isLenient = true  
  explicitNulls = false  
  }  
  )  
  }  

  // Default request for POST, PUT, DELETE,etc...  
  install(DefaultRequest) {  
  header(HttpHeaders.ContentType, ContentType.Application.Json)  
  //add this accept() for accept Json Body or Raw Json as Request Body  
  accept(ContentType.Application.Json)  
  }  

 }  
}

Create a repository ApiRepository,kt to provide us with the required data from the Api endpoint.

package com.rhytham.ktorclient.Network  

import com.rhytham.ktorclient.Model.Post  
import com.rhytham.ktorclient.Network.ApiClient.client  
import io.ktor.client.call.body  
import io.ktor.client.request.delete  
import io.ktor.client.request.get  
import io.ktor.client.request.post  
import io.ktor.client.request.put  
import io.ktor.client.request.setBody  

class ApiRepository {  

  suspend fun getAllPosts(): List<Post> = client.get(ApiRoutes.BLOG_POST).body()  

  suspend fun createNewPost(newPost: Post): Post = client.post(ApiRoutes.BLOG_POST){  
  setBody(newPost)  
  }.body()  

  suspend fun updatePost(id:Int, post: Post):Post = client.put(ApiRoutes.BLOG_POST+"/$id"){  
  setBody(post)  
  }.body()  

  suspend fun deletePost(id:Int) = client.delete("${ApiRoutes.BLOG_POST}/$id")  
}

For UI Code Checkout KtorClientExample (github.com)

Jetpack UI integration

Card Compose

Create a CardCompose to show the response from the API endpoints.

@Composable  
fun CodeCard(jsonStr: String) {  
  val scrollState = rememberScrollState()  

  Card(modifier = Modifier  
  .fillMaxWidth()  
 .verticalScroll(  
  state = scrollState,  
  enabled = true  
  )  
 .padding(all = 8.dp),  
 ) {  
  Text(  
  modifier = Modifier.padding(start = 12.dp, top=12.dp),  
  text = "response: ",  
  style = MaterialTheme.typography.labelSmall,  
  fontFamily = FontFamily.Monospace  
        )  
  Text(  
  modifier = Modifier.padding(all = 12.dp),  
  text = jsonStr,  
  fontFamily = FontFamily.Monospace  
        )  
  }  

}

Looks like:

enter image description here

Custom FilterGroup

Create a Custom Filter Group.

enter image description here

Get code from : Custom FilterChip Group using Jetpack Compose in Android

HomeScreen UI

Right click and create a new package Screen inside the Screen package create a kotlin file HomeScreen.kt.

  1. Define the HomeScreen function with the @Composable annotation.

  2. Within the HomeScreen function, create a Column composable.

@Composable  
fun HomeScreen() {
}
  1. Define a list of chipsList containing strings representing different HTTP request types and use rememberCoroutineScope to create a coroutine scope called scope.
val chipsList = listOf("/POST", "/GET", "/DELETE", "/PUT")  
// Code 
val scope  = rememberCoroutineScope()
  1. Create a mutableStateOf variable called jsonRepose that initially holds an empty string.

  2. Create an instance of an ApiRepository class.

val apiRepo = ApiRepository()
  1. Create a mutableStateOf variable called showLoading that initially holds false. Define a Post object with some sample data.
var showLoading by remember { mutableStateOf(false) }
  1. Display the headline variable as a Text composable element .

  2. Use the FilterChipGroup composable element to display the chipsList and handle the selection of different HTTP request types. When a chip is selected, update the headLine variable and reset the jsonRepose variable to an empty string.

FilterChipGroup(items = chipsList,  
  onSelectedChanged = { selectedIndex:Int ->  
  headLine = chipsList[selectedIndex]  
  jsonRepose =""  
  })
  1. Display a Button composable element that sends the HTTP request when clicked. When the button is clicked, set the showLoading variable to true. Inside a coroutine scope, use a when expression to check the value of headLine and call the appropriate method on the ApiRepository class to make the HTTP request. Set the jsonRepose variable to the response of the HTTP request. Finally, set the showLoading variable back to false.
Button(modifier = Modifier  
  .align(alignment = Alignment.CenterHorizontally)  
 .width(200.dp),  
  onClick = {  
  showLoading = true  
  scope.launch {  
  when(headLine){  
  "/POST" -> {  
  jsonRepose = apiRepo.createNewPost(post).toString()  
 }  "/GET" -> {  
  jsonRepose = apiRepo.getAllPosts().toString()  

 }  "/PUT" -> {  
  // Use PUT request to Update data  
  jsonRepose = apiRepo.updatePost(  
  id=1,  
  post = post ).toString()  

 }  "/DELETE" -> {  
  jsonRepose = apiRepo.deletePost( id = 2).status.toString()  

 } }  showLoading = !showLoading  
  }  

 }) {  
  Text(text = "Send")  
}
  1. If showLoading is true, display a LinearProgressIndicator composable element.
if(showLoading) {  
  LinearProgressIndicator( modifier = Modifier.fillMaxWidth())  
}
  1. Display a custom CodeCard composable element that displays the JSON response in a formatted way.
CodeCard(jsonStr = jsonRepose)

That's it! We can now use this HomeScreen function as a composable element in our app to allow users to make HTTP requests and display the response.

App Mockup

Get the Source Code from GitHub: KtorClientExample

Conclusion

Ktor is a framework to easily build connected applications – web applications, HTTP services, and mobile and browser applications. Modern connected applications need to be asynchronous to provide the best experience to users, and Kotlin Coroutines provide awesome facilities to do it easily and straightforwardly. The goal of Ktor is to provide an end-to-end multiplatform framework for connected applications. It enables asynchronous programming with minimal boilerplate code.

In this tutorial, we have learned how we can use the Ktor client and perform HTTP requests. We have used the response and displayed the data using Jetpack Compose.