Making API request using Ktor client in Android

Difficulty: Beginner

Ktor is an excellent Http client for android it has many features such as web sockets, monitoring progress when uploading a file and the ability to use it in KMM projects across different operating system like Android and IOS.

In this article we will create a simple app that makes an API request using FakeApi and displays the list of posts returned by the API in a Recycler-View.

Important note: the best practice is to use a dependency injection framework like Dagger Hilt and provide your Ktor instance that way, but for simplicity in this tutorial we are not going to use any dependency injection framework.

Setup

Add the internet permisson in you AndroidManifest.xml file

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

add the Ktor dependencies in your module’s build.gradle file, you can find the latest version here.

  • Compose
  • XML


    val  ktor_version = "2.2.3"
    implementation ("io.ktor:ktor-client-core:$ktor_version")
    implementation ("io.ktor:ktor-client-android:$ktor_version")
    implementation ("io.ktor:ktor-client-serialization:$ktor_version")
    implementation ("io.ktor:ktor-client-logging:$ktor_version")
    implementation ("io.ktor:ktor-client-content-negotiation:$ktor_version")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")

    val lifecycle_version = "2.8.5"

    // ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
    // ViewModel utilities for Compose
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")

    // for loading the image (optional)
    implementation("io.coil-kt:coil-compose:2.7.0")
    id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0"
    plugins {
    id 'org.jetbrains.kotlin.plugin.serialization'
}

//for viewModels and viewModelScope
    implementation 'androidx.activity:activity-ktx:1.6.1'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
   
def ktor_version = "2.2.3"
    implementation "io.ktor:ktor-client-core:$ktor_version"
    implementation "io.ktor:ktor-client-android:$ktor_version"
    implementation "io.ktor:ktor-client-serialization:$ktor_version"
    implementation "io.ktor:ktor-client-logging:$ktor_version"
    implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
    implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"

    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0"
plugins {
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.0'
}

Designing our UI

  • Compose
  • XML
@Composable
fun ScreenOneContent(modifier: Modifier = Modifier) {
    val postsViewModel: PostsViewModel = viewModel()
    LazyColumn(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), modifier = Modifier.fillMaxSize()) {
        item {
            if (postsViewModel.postsList.isEmpty()) {
                CircularProgressIndicator()
            }
        }
        items(postsViewModel.postsList){ post ->
            PostItem(modifier = Modifier.fillMaxWidth(), title = post.title, content = post.content, picture = post.picture)
        }
    }
}

@Composable
fun PostItem(modifier: Modifier = Modifier, title: String, content: String , picture: String) {
    Card (modifier = Modifier) {
        Column(modifier.padding(8.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
            
            Text(title, fontSize = 24.sp, fontWeight = FontWeight.Bold)
            Text(content, fontSize = 16.sp)
        }
    }
}

In our main activity add a recyclerview like below.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.posts.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_posts"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ProgressBar
        android:id="@+id/pb_posts"
        style="?android:attr/progressBarStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.5" />

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:backgroundTint="#45F0F4C3"
    app:cardCornerRadius="8dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="16dp">

        <ImageView
            android:id="@+id/iv_picture"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="32dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="32dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:srcCompat="@tools:sample/avatars" />

        <TextView
            android:id="@+id/tv_username"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="7dp"
            app:layout_constraintEnd_toEndOf="@+id/iv_picture"
            app:layout_constraintTop_toBottomOf="@+id/iv_picture"
            tools:text="user" />

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="32dp"
            android:layout_marginTop="13dp"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium"
            app:layout_constraintEnd_toEndOf="@+id/tv_username"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_username"
            tools:text="Let me explain" />

        <TextView
            android:id="@+id/tv_content"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="32dp"
            android:layout_marginTop="17dp"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium"
            app:layout_constraintEnd_toEndOf="@+id/tv_title"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_title"
            tools:text="But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects," />

    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

In this example we are going to use the end point ‘/posts’ in our ‘model’ package Lets create a Data class to represent the JSON that we will receive from the server, notice that we annotate the class with ‘@Serializable’ this annotation allows Ktor to map the JSON key value pairs with our Post class.

@Serializable
data class Post(
    val id: String,
    val title: String,
    val content: String,
    val slug: String,
    val picture: String,
    val user: String
)
{"id":"1","title":"Let me explain","content":"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects,","slug":"let-me-explain","picture":"https:\/\/fakeimg.pl\/350x200\/?text=FreeFakeAPI","user":"\/api\/users\/1","_links":{"self":{"href":"\/api\/posts\/1"},"modify":{"href":"\/api\/posts\/1"},"delete":{"href":"\/api\/posts\/1"}}}
  • Compose
  • XML

Nothing To Do

Now lets create an Adapter class for our recyclerview items, create a package ‘ui’ inside ‘ui’ create another package lets call it ‘posts’ for our main screen inside it create the PostAdapter.

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.ktorsampleapp.databinding.PostItemBinding
import com.example.ktorsampleapp.model.remote.Post

/**
 * Created by Taha Ben Ashur (https://github.com/tahaak67) on 07,Feb,2023
 */
class PostAdapter : ListAdapter<Post, RecyclerView.ViewHolder>(DIFF_CALLBACK){
    companion object{
        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Post>() {

            override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean {
                return oldItem.title == newItem.title || oldItem.content == newItem.content || 
                        oldItem.picture == newItem.picture
            }

        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
            RecyclerView.ViewHolder {
        val  binding: PostItemBinding =
            PostItemBinding.inflate(LayoutInflater.from(parent.context),parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is ViewHolder){
            val item = getItem(position)
            holder.bind(item)
        }
    }


    inner class ViewHolder(val itemBinding: PostItemBinding):
        RecyclerView.ViewHolder(itemBinding.root) {
        fun bind(item: Post){
            itemBinding.apply {
                tvTitle.text = item.title
                tvContent.text = item.content
            }
        }
    }
}

Creating the Ktor Client

In our ‘model’ package create a new Kotlin object lets call it “Provider”, this object will hold a reference to our Ktor client instance.

import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

/**
 * Created by Taha Ben Ashur (https://github.com/tahaak67) on 07,Feb,2023
 */
object Provider {
    val client: HttpClient = HttpClient(Android) {

        //to map json objects returned from the api to a kotlin data class
        install(ContentNegotiation) {
            json(Json {
                //ignores json keys we have not included in our data class
                ignoreUnknownKeys = true
            })
        }
        //a logger to see logging information about every request we make using the client
        install(Logging) {
            level = LogLevel.ALL
        }

    }
}

In our ‘model’ package create a new interface, this interface will include all the functions needed to communicate with our API, this can be called “PostsService” or “PostsApi”.

interface PostsApi {
    suspend fun getPosts(): List<Post>
}

Next we will need a place to store the base url to our api and our route, for this i will create a Kotlin object in the ‘model’ package and call it ‘EndPoints’

object EndPoints {
    private const val BASE_URL = "https://freefakeapi.io/api/"
    const val POSTS = "$BASE_URL/posts"
}

Lets create an implementation for our PostApi interface, this will have the actual logic that uses the client to make the GET request.

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*

/**
 * Created by Taha Ben Ashur (https://github.com/tahaak67) on 04,Feb,2023
 */
class PostsApiImpl(
    private val client: HttpClient
): PostsApi {

    override suspend fun getPosts(): List<Post> {
        return try {
            client.get {
                url(Routes.POSTS)
            }.body()
        } catch (e: RedirectResponseException) {
            Log.e("PostsApi", "3XX Error: ${e.message}")
            emptyList()
        } catch (e: ClientRequestException) {
            Log.e("PostsApi", "4XX Error: ${e.message}")
            emptyList()
        } catch (e: ServerResponseException) {
            Log.e("PostsApi", "5XX Error: ${e.message}")
            emptyList()
        } catch (e: Exception) {
            Log.e("PostsApi", "Error: ${e.message}")
            emptyList()
        }
    }
}

Creating the ViewModel

Now that we implemented our API service we need to make the request somewhere the best place to do this is inside a viewModel class, in our ‘posts’ package lets create a new class and call it PostsViewModel make sure the class inherits ViewModel from ‘androidx.lifecycle’.

import androidx.lifecycle.ViewModel

class PostViewModel: ViewModel() {
}
  • Compose
  • XML

Next create an instance of the PostsApiImpl and make the api call in the init block of the viewModel

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.democlass.data.PostItem
import io.ktor.client.call.body
import io.ktor.client.request.get
import kotlinx.coroutines.launch

/* Created by Taha https://github.com/tahaak67/ at 5/9/2024 */

class PostsViewModel: ViewModel() {

    private var postsApi: PostsApi = PostsApiImpl(Provider.client)

    var postsList by mutableStateOf(emptyList<PostItem>())
        private set

    init {
        viewModelScope.launch {
            postsList = postsApi.getPosts()
        }
    }

}

Next create an instance of the PostsApiImpl and create a function that calls getPosts() and stores the results in a LiveData object.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.ktorsampleapp.model.remote.Post
import com.example.ktorsampleapp.model.remote.PostsApi
import com.example.ktorsampleapp.model.remote.PostsApiImpl
import com.example.ktorsampleapp.utils.Provider
import kotlinx.coroutines.launch

/**
 * Created by Taha Ben Ashur (https://github.com/tahaak67) on 07,Feb,2023
 */
class PostViewModel: ViewModel() {

    private var postsApi: PostsApi = PostsApiImpl(Provider.client)
    private val _posts: MutableLiveData<List<Post>> = MutableLiveData()
    val posts: LiveData<List<Post>> get() = _posts
    
    init {
        loadPostsFromApi()
    }

    fun loadPostsFromApi(){
        viewModelScope.launch { 
            _posts.value = postsApi.getPosts()
        }
    }
}

Using the View Model

  • Compose
  • XML

Call the viewmodel using the compose viewModel() function like below

@Composable
fun ScreenOneContent(modifier: Modifier = Modifier) {
    val postsViewModel: PostsViewModel = viewModel()
    LazyColumn(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), modifier = Modifier.fillMaxSize()) {
        item {
            if (postsViewModel.postsList.isEmpty()) {
                // if list is empty show a progress indicator
                CircularProgressIndicator()
            }
        }
        items(postsViewModel.postsList){ post ->
            PostItem(modifier = Modifier.fillMaxWidth(), title = post.title, content = post.content, picture = post.picture)
        }
    }
}

Finally we use the view model in our activity and observe changes to our live data

import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.ktorsampleapp.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel: PostViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        val adapter = PostAdapter()
        binding.rvPosts.adapter = adapter

        //start observing our data
        viewModel.posts.observe(this) { posts ->
            if (posts.isEmpty()) {
                //show a progress bar if the list is empty
                binding.pbPosts.visibility = View.VISIBLE
            } else {
                //otherwise hide the progress bar 
                binding.pbPosts.visibility = View.GONE
                adapter.submitList(posts)
            }
        }
    }
}

Results

When we lunch our app we should see the following:

Extra: Displaying images using coil

Lets use the Coil library to load the images into our image views, first add the coil dependency to build.gradle(module), find the latest version here.

  • Compose
  • XML

you can already find the dependency in the first section above to use it we just need to call Async image in our ‘PostItem’ composable like below:


AsyncImage(model = picture, contentDescription = "", modifier = Modifier.size(150.dp))
implementation 'io.coil-kt:coil:2.2.2'

in our PostAdapter in the bind function add the following line

ivPicture.load(item.picture)

And now our posts have pictures!

as always the source code is available on Github. Compose version Here.

5 2 votes
Article Rating
Subscribe
Notify of
guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

[…] this example i am going to use the project from my article Making API request using Ktor client in Android as a starter project for this tutorial please bare in mind that this tutorial is not intended for […]