Hello everyone! Today, we’re exploring a new way to handle network calls in Android using the “Kotlin KTor Android Client”. While Retrofit is a good choice, we’ll also look into situations where using KTor could be helpful. If you’re using Retrofit now, no worries – both options work well, and we’ll talk about when choosing the Kotlin KTor Android Client might be a good idea.
This tutorial is a segment of my free course titled “Architecting Excellence: A Deep Dive into Multi-Module Android Apps.” You can find the complete course on YouTube, where I delve into essential topics in modern Android app development. Below, you’ll find the video corresponding to this post, offering a visual walkthrough. Feel free to check it out if you prefer a more interactive learning experience.
If you’ve landed here solely for the source code of this project, you can find it below.
Kotlin Ktor Android Client
Kotlin has a handy tool called Ktor. It helps you build servers and clients that can handle tasks asynchronously. So, if you want to make RESTful APIs for your app using Kotlin, you’re in luck. I’ve got a detailed guide on how to do just that with the Ktor Framework. But what’s even cooler about Ktor is its Android client. This means you can use it to easily manage network tasks in your Android apps. Pretty neat, huh?
In this post, we’ll learn about using the Kotlin Ktor Android Client in our Android project. But before we get into that, let’s talk about why you might choose Ktor when you already have Retrofit.
While Retrofit remains a reliable option, there’s no compelling reason to suggest migrating to Ktor Android Client unless you’re working with Kotlin Multiplatform. Retrofit doesn’t support Kotlin Multiplatform, and nowadays, many developers are opting for a consistent data layer across Android and iOS apps using Kotlin Multiplatform. If your project doesn’t involve Kotlin Multiplatform, sticking with Retrofit is perfectly fine. I’ll highlight some advantages of Ktor below, but none of them is a dealbreaker for Retrofit. So, if you’re comfortable with Retrofit in your project, that’s absolutely fine.
Advantages of Ktor Android Client
- Coroutines Ease: Ktor makes writing asynchronous code a breeze by fully embracing Kotlin Coroutines.
- Feature-Rich: Unlike Retrofit, which is mainly for API calls, Ktor goes beyond that. It’s a feature-rich framework that allows you to build entire backends and supports various protocols, not just HTTP.
- Concise Code: Kotlin is known for being concise, and Ktor inherits this trait. This means your code can be more straightforward to understand.
- Kotlin Consistency: Ktor is a pure Kotlin framework, that provides consistency throughout your project. If your team uses Kotlin everywhere, Ktor seamlessly fits in, making it handy for Kotlin-centric developers.
- Built-in Goodies: Ktor comes with built-in features for many tasks like Content-Negotiation, Authentication, OAuth, and more. This can save you time and effort in handling common functionalities.
Alright, no more talking – let’s dive into some hands-on implementation. We’ll see how to use the Ktor Android Client to handle networking in your Android app.
Setting Up Dependencies
Whether you’re starting a new project in Android Studio or working with an existing one, the first thing to do is set up the Ktor Android client dependencies.
In your project’s app-level build.gradle file, just make sure to add these dependencies in the dependencies block. This simple step ensures that your project is ready to use Ktor’s Android client features smoothly.
1 2 3 4 5 6 7 8 |
implementation("io.ktor:ktor-client-core:2.3.5") implementation("io.ktor:ktor-client-cio:2.3.5") implementation("io.ktor:ktor-client-logging:2.3.5") implementation("io.ktor:ktor-client-content-negotiation:2.3.5") implementation("io.ktor:ktor-client-auth:2.3.5") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5") |
As we are going to use kotlinx serialization, we also need to add kotlinx serialization plugin.
1 2 3 |
id("org.jetbrains.kotlin.plugin.serialization") |
Once you’ve added the mentioned dependencies, your project is all set to make use of Ktor. Now, you can easily integrate and use Ktor features in your Android project.
Creating an HttpClientBuilder
Let’s architect a class that acts as an HttpClient provider, seamlessly facilitating the execution of network requests. By leveraging the builder pattern for enhanced ease of use and comprehension, this class guarantees a direct and user-friendly approach to obtaining the HttpClient object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
class MyHttpClientBuilder { private var protocol: URLProtocol = URLProtocol.HTTP private lateinit var host: String fun protocol(protocol: URLProtocol) = apply { this.protocol = protocol } fun host(host: String) = apply { this.host = host } fun build(): HttpClient { return HttpClient(CIO) { expectSuccess = true engine { endpoint { keepAliveTime = 5000 connectTimeout = 5000 connectAttempts = 3 } } defaultRequest { url { protocol = this@MyHttpClientBuilder.protocol host = this@MyHttpClientBuilder.host } header(HttpHeaders.ContentType, "application/json") } install(ContentNegotiation) { json( Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true }, ) } install(Auth) { bearer { loadTokens { BearerTokens("your access token", "your refresh token") } refreshTokens { BearerTokens("your access token", "your refresh token") } } } install(Logging) { logger = object : Logger { override fun log(message: String) { println(message) } } level = LogLevel.ALL } } } } |
Understanding our HttpClientBuilder
The given code defines a Kotlin class named MyHttpClientBuilder
. This class is responsible for creating and configuring instances of the HttpClient, a component used for making network requests. Now, let’s go through the key components and functionalities step by step.
- Class Structure:
- The class has private properties, namely
protocol
andhost
, used to store the URL protocol and host details. - It provides three functions:
protocol
,host
, andbuild
.
- The class has private properties, namely
- Protocol and Host Configuration:
- The
protocol
function allows the user to set the URL protocol (defaulting to HTTP) and returns the modified builder instance. - The
host
function enables the user to set the host for the network request and returns the modified builder instance.
- The
- Building the HttpClient:
- The
build
function creates and configures an instance of HttpClient using the CIO (Coroutine I/O) engine. - It sets various configurations for the engine, such as keep-alive time, connect timeout, and attempts for connection retries.
- The
- Default Request Configuration:
- The default request configuration includes setting the URL protocol and host based on the values provided.
- Additionally, it sets the request header to indicate that the content type is JSON.
- Content Negotiation:
- Content negotiation is installed to handle JSON serialization and deserialization.
- It uses the kotlinx.serialization library with specific settings like pretty printing and leniency.
- Authentication:
- Authentication is installed with a bearer token approach.
- The code includes a mechanism for handling access and refresh tokens, which are crucial for authentication. It’s important to note that at this point, no tokens are provided explicitly. If required, you can supply the tokens, or alternatively, retrieve them from your session handler instance. I’ve extensively covered session handling in my course titled “Architecting Excellence: A Deep Dive into Multi-Module Android Apps.”
- Logging:
- Logging is configured to print log messages to the console with a specified logger implementation.
- The log level is set to capture all log messages.
- Returning the Configured HttpClient:
- The final configured HttpClient instance is returned from the
build
function.
- The final configured HttpClient instance is returned from the
In summary, the MyHttpClientBuilder
class simplifies the creation of HttpClient instances by providing a fluent interface for configuring various aspects like protocol, host, engine settings, default request headers, content negotiation, authentication, and logging. The builder pattern makes it easy to customize and create HttpClient instances with the desired configurations.
Consuming HttpClientBuilder to perform network calls
We have the builder that we can use to get HttpClient, and with the help of HttpClient we can perform network calls.
To make our network call, we need a REST API. If you want to learn how to create RESTful APIs with Ktor, there’s a helpful tutorial you can watch here. But for now, you can find dummy REST APIs easily by searching on Google. I found one that we can use for this example.
1 2 3 |
https://reqres.in/api/users |
Mapping JSON Response to Data Class
To properly map the response from the given URL, we’ll need the following data class. In this instance, I’ve utilized the @Serializable
and @SerialName
annotations, both of which are part of the kotlinx.serialization library we included in our project. These annotations play a crucial role in defining how the response is mapped to our data class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Serializable data class UserResponse( @SerialName("data") val users: List<User>, @SerialName("page") val page: Int, @SerialName("per_page") val perPage: Int, @SerialName("total") val total: Int, @SerialName("total_pages") val totalPages: Int ) { @Serializable data class User( @SerialName("avatar") val avatar: String, @SerialName("email") val email: String, @SerialName("first_name") val firstName: String, @SerialName("id") val id: Int, @SerialName("last_name") val lastName: String ) } |
Performing Network Call
We’ll execute a basic HTTP GET request using the HTTP client. In this context, I’ve established a MainViewModel where the call is being initiated.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class MainViewModel(private val httpClient: HttpClient) : ViewModel() { private val _users = MutableStateFlow<List<UserResponse.User>>(emptyList()) val users: StateFlow<List<UserResponse.User>> = _users init { getUsers() } private fun getUsers() = viewModelScope.launch { val response: UserResponse = httpClient.get { url("api/users") }.body() _users.value = response.users } } |
The MainViewModel
class extends the ViewModel
class and takes an instance of HttpClient
as a parameter. This class is responsible for handling user-related data and making HTTP requests.
- StateFlow for Users:
- Inside the class, there’s a
MutableStateFlow
named_users
that holds a list ofUserResponse.User
objects. This is essentially the data we want to observe and react to. - The
users
property is a publicStateFlow
derived from_users
. It provides external access to the user data.
- Inside the class, there’s a
- Initialization:
- In the
init
block, thegetUsers
function is called to initiate the process of fetching user data as soon as theMainViewModel
is created.
- In the
- getUsers Function:
- The
getUsers
function is a coroutine launched within theviewModelScope
. This ensures that it operates within the lifecycle of the associatedViewModel
. - It utilizes the injected
httpClient
to make an HTTP GET request. - The request is made to the “api/users” endpoint using the
url
function. - The response is of type
UserResponse
, and it represents the structure of the expected JSON response from the API.
- The
- Updating StateFlow:
- The obtained
response.users
(list of users) is assigned to_users.value
, updating the StateFlow. - This triggers observers (such as UI components observing
users
) to react to the new user data.
- The obtained
In essence, the MainViewModel
orchestrates the retrieval of user data from a specified API endpoint using the provided httpClient
. The fetched data is then exposed through a StateFlow
, allowing other components to observe and react to changes in the user data.
Consuming the Result in UI
Now that we’ve completed all the necessary steps, we have a list of users ready to be consumed and displayed on the UI. In this example, I’ve incorporated this user data consumption in the MainActivity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class MainActivity : ComponentActivity() { private val httpClientBuilder = MyHttpClientBuilder() .protocol(URLProtocol.HTTPS) .host("reqres.in") .build() private val viewModel: MainViewModel by viewModels<MainViewModel> { object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return MainViewModel(httpClientBuilder) as T } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { KtorclientTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { val users = viewModel.users.collectAsState() Users(users = users.value) } } } } } @Composable fun Users(users: List<UserResponse.User>) { LazyColumn { items(users) { Text(text = it.firstName) } } } |
Now, go ahead and execute your project, ensuring that you include the necessary internet permission in your manifest file. This step is crucial for enabling network connectivity.
Addressing Project’s Shortcomings
There are a couple of issues in this project that merit attention.
- Firstly, the creation of a ViewModelFactory to supply the HttpClient instance introduces unnecessary boilerplate code. This can be alleviated by incorporating dependency injection.
- Secondly, the tutorial skips handling the scenario where the API call fails for the sake of simplicity.
However, for a more comprehensive understanding, I recommend referring to my course “Architecting Excellence: A Deep Dive into Multi-Module Android Apps.” In that course, every aspect of this concern is thoroughly covered.
Concluding this post, I trust you found it enjoyable and insightful. If you encounter any issues, questions, or confusion, please feel free to leave your comments below. Crafting articles of this nature demands considerable effort, and your feedback is immensely valuable. If you believe this content has offered you valuable insights, I urge you to share this newfound knowledge with your friends. Your support not only acknowledges the effort invested but also helps in expanding our community of knowledge seekers. Thank you for your time and engagement!