Jetpack Compose Ktor: How to get API data

Introduction

In modern Android development, Jetpack Compose and Ktor make it easier to build user interfaces and interact with APIs, respectively. Jetpack Compose simplifies UI development with a declarative approach, while Ktor provides an efficient way to perform HTTP requests. In this article, we'll explore how to combine these two powerful tools to fetch data from an API using Ktor and display it in a Jetpack Compose UI. The example involves creating an Android application that fetches a list of users from a placeholder API and presents it in a scrollable list format.

The project consists of three main components: the KtorClient to handle network requests, a function to fetch user data from an API, and the MainActivity that sets up the Jetpack Compose UI to display the fetched data. Each component plays a crucial role in ensuring that the app communicates effectively with the API and presents the data in a user-friendly manner.

KtorClient Setup

The KtorClient.kt file configures an instance of Ktor's HTTP client. It sets up the basic network configurations required for API calls, including JSON serialization, logging, and timeouts.

First, a custom Json object is initialized, allowing the app to encode default values and ignore unknown keys from API responses. This is particularly useful when dealing with APIs that may return fields not explicitly modeled in your app. The HttpClient is then initialized with several features:

  • JsonFeature: This feature uses KotlinxSerializer to serialize and deserialize JSON data.
  • Logging: This feature logs HTTP requests and responses. Here, a custom logger writes log messages using Android's Log class, making it easy to track network activity in the Android logcat.
  • HttpTimeout: This feature sets a timeout for socket, request, and connection operations, ensuring that the app doesn't hang indefinitely if the server is slow or unresponsive. Finally, a default request configuration ensures that all outgoing requests expect and send JSON content.

API Request Function

The APICaller.kt file contains the getUsers function, which is responsible for fetching user data from the API. This function is declared suspend to leverage Kotlin's coroutines for asynchronous network calls. Using the KtorClient instance, it makes a GET request to "https://jsonplaceholder.typicode.com/todos," a public API that returns a list of user tasks.

The response is automatically deserialized into a list of User objects, thanks to the JSON serialization feature of Ktor. The User data class is annotated with @Serializable, which allows Kotlin to automatically handle JSON serialization and deserialization for this model. The User class contains four properties: userId, id, title, and completed, corresponding to the fields returned by the API.

MainActivity and Jetpack Compose UI

The MainActivity.kt file defines the main entry point for the application. It uses Jetpack Compose to build a simple UI that displays a list of user tasks. The MainActivity calls the MainContent composable, which is responsible for laying out the user interface.

Within the MainContent composable, a produceState function is used to asynchronously fetch the user data by calling getUsers(). The produceState function ensures that the user data is fetched only once when the composable is first displayed. As the data is fetched, it updates the UI with the user tasks in a LazyColumn, which is a performant way to display a large list of items in a scrollable view.

Each user task is represented by a Card composable, which dynamically adjusts its background color based on whether the task is completed. Inside the card, the Row composable arranges the content horizontally, including a circular colored box displaying the user's ID and a Text composable showing the task title.

Gradle Setup

The project requires specific dependencies to enable Ktor's functionality. In the build.gradle file for the project, the Kotlin serialization plugin is added, allowing the use of Kotlin's built-in serialization library. The app module's build.gradle includes several dependencies:

  • ktor-client-android: This is the core Ktor client library for Android.
  • ktor-client-serialization: Enables JSON serialization using Ktor.
  • ktor-client-logging-jvm: Adds logging capabilities for network requests.

Additionally, the org.jetbrains.kotlinx:kotlinx-serialization-json dependency is included to handle JSON serialization, making it easier to work with JSON data returned by APIs.

Summary

By combining Jetpack Compose and Ktor, Android developers can build modern, performant applications that interact with APIs seamlessly. In this example, we configured a Ktor client with essential features like JSON serialization and logging, and used it to fetch user data from an API. The Jetpack Compose UI efficiently displayed the fetched data using a LazyColumn, offering a smooth user experience.

This approach highlights the power of Kotlin's coroutines and Jetpack Compose's declarative UI system, allowing for asynchronous operations to be handled elegantly and user interfaces to be updated dynamically based on real-time data. With this setup, developers can build responsive and network-enabled Android apps with minimal effort.


KtorClient.kt

package com.cfsuman.composenetwork

import android.util.Log
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.serialization.json.Json

object KtorClient{
    private val json = Json {
        encodeDefaults = true
        ignoreUnknownKeys = true
    }

    val httpClient = HttpClient {
        install(JsonFeature){
            serializer = KotlinxSerializer(json)
        }

        install(Logging){
            logger = object : Logger {
                override fun log(message: String) {
                    Log.d("xapp-ktor", message )
                }
            }
            level = LogLevel.ALL
        }

        install(HttpTimeout){
            socketTimeoutMillis = 15_000
            requestTimeoutMillis = 15_000
            connectTimeoutMillis = 15_000
        }

        defaultRequest {
            contentType(ContentType.Application.Json)
            accept(ContentType.Application.Json)
        }
    }
}
APICaller.kt

package com.cfsuman.composenetwork

import io.ktor.client.request.*
import io.ktor.utils.io.core.*
import kotlinx.serialization.Serializable


suspend fun getUsers(): List<User> {
    return KtorClient.httpClient.use {
        it.get("https://jsonplaceholder.typicode.com/todos")
    }
}

@Serializable
data class User(
    val userId: Int,
    val id: Int,
    val title: String,
    val completed: Boolean
)
MainActivity.kt

package com.cfsuman.composenetwork

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.cfsuman.composenetwork.ui.theme.ComposeNetworkTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeNetworkTheme {
                MainContent()
            }
        }
    }
}


@Composable
fun MainContent() {
    Log.d("xapp-called","Compose")

    val users = produceState(
        initialValue = listOf<User>(),
        producer = {
            value = getUsers()
            Log.d("xapp-called","Producer")
        }
    )

    LazyColumn(
        Modifier.fillMaxSize(),
        contentPadding = PaddingValues(12.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ){
        items(users.value){ user->
            Card(
                modifier = Modifier.fillMaxWidth(),
                elevation = 2.dp,
                backgroundColor = if (user.completed)
                    Color(0xFFB6F1B8) else Color.White
            ) {
                Row(
                    Modifier.padding(12.dp),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.spacedBy(12.dp)
                ) {
                    Box(
                        Modifier
                            .clip(CircleShape)
                            .background(Color(0xFF4CAF50))
                            .size(48.dp),
                        contentAlignment = Alignment.Center
                    ) {
                        Text(text = "${user.id}")
                    }
                    Text(text = user.title)
                }
            }
        }
    }
}
build.gradle [project]

buildscript {
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-serialization:1.6.10"
    }
}
build.gradle [app]

plugins {
    id 'kotlinx-serialization'
}


dependencies {
    implementation 'io.ktor:ktor-client-android:1.6.8'
    implementation 'io.ktor:ktor-client-serialization:1.6.8'
    implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0'
    implementation 'io.ktor:ktor-client-logging-jvm:1.6.8'
}
More android jetpack compose tutorials