Chapter 6: Networking (APIs) - The Ultimate Guide
The Big Picture: What is Networking?
Modern mobile apps are rarely "offline." Your app needs to be dynamic. It needs to fetch the latest news, get user profile data, upload photos, and check the weather. This process of communicating with a "server" over the internet is called **Networking**.
The "server" is just another computer (or a fleet of computers) whose job is to store data and provide it to clients (like your Android app, a website, or an iOS app). This communication doesn't happen randomly; it follows a strict set of rules, or a **protocol**. The most common protocol for the web is **HTTP (Hypertext Transfer Protocol)**.
The Client-Server Model
All networking is based on this model.
- Client: The device *requesting* data (your Android app).
- Server: The device *serving* data (the backend, e.g.,
https://api.google.com).
Synchronous vs. Asynchronous (The "Why" of Coroutines)
This is a critical concept. Imagine you (the UI Thread) are taking an order at a drive-thru.
- Synchronous (Bad): You take an order, walk to the kitchen, *wait* there for 10 minutes for the food to be cooked, then walk back. While you are "blocked" waiting, **no one else can place an order**. The entire line of cars is stuck. This is what happens if you do networking on the UI thread. Your app *freezes* and crashes (an "ANR" - Application Not Responding).
- Asynchronous (Good): You take an order, *give it to the kitchen*, and immediately go back to the window to take the next order. When the food is ready, the kitchen *notifies* you, and you just deliver it. This is **non-blocking**.
In Android, you **must** perform all network requests asynchronously (on a background thread). In Chapter 1, we learned about **Kotlin Coroutines**. Coroutines, along with suspend functions, are the modern, easy way to manage this background work without the "callback hell" of older methods.
The API: Your App's "Waiter"
How does your app talk to the server? It uses an **API (Application Programming Interface)**. We covered this in the blog, but let's review the "Restaurant Analogy" in detail.
[attachment_0](attachment)- You (The Customer): You are the "client" (your Android app). You know what you *want* (e.g., "I want all posts from this blog").
- The Kitchen (The Server/Database): This is where all the data (the food) is stored and prepared. The kitchen is complex, and you can't just walk in.
- The Waiter (The API): The API is the messenger that your app talks to. You give your request to the waiter (API) in a specific format (an "HTTP Request"). The waiter takes this request to the kitchen (server), gets the data (food), and brings it back to you in a neat package (a "JSON Response").
HTTP & REST: The Rules of Communication
Your app (client) and the server talk to each other using **HTTP Requests**. An HTTP request has two main parts:
- An HTTP Method (Verb): This tells the server *what kind* of action you want to perform.
- An Endpoint (URL): This tells the server *what* data you want to act upon.
This set of rules, using HTTP verbs and URLs to manage data, is called **REST (REpresentational State Transfer)**. An API that follows these rules is called a **RESTful API**.
The Main HTTP Verbs (CRUD)
CRUD stands for **Create, Read, Update, Delete**. These are the four basic operations of data storage.
- GET (Read): Used to **retrieve** data. This is safe and can be called many times without changing data (it is "idempotent").
GET /api/users(Get a list of all users)
GET /api/users/123(Get details for user 123)
- POST (Create): Used to **create** a new resource. You send data (like a new user's info) in the "body" of the request. This is *not* idempotent (calling it twice will create two users).
POST /api/users(Create a new user)
- PUT (Update/Replace): Used to **update** an existing resource by *completely replacing* it. This is idempotent (calling it twice with the same data has the same result).
PUT /api/users/123(Replace user 123 with new info)
- PATCH (Update/Modify): Used to **update** an existing resource by *partially modifying* it. You only send the fields you want to change (e.g., just the email).
PATCH /api/users/123(Update only the email for user 123)
- DELETE (Delete): Used to **delete** a resource.
DELETE /api/users/123(Delete user 123)
- HEAD: Same as
GET, but it *only* requests the headers, not the actual data. Useful for checking if a resource exists or has changed.
JSON: The Language of APIs
When the server sends data back, it needs to be in a format your app can understand. It can't just send a raw database file. The most popular format by far is **JSON (JavaScript Object Notation)**.
It's a lightweight, human-readable format that looks very similar to a Kotlin Map or Data Class. Your job as a developer is to **parse** this JSON text into native Kotlin objects that your app can use.
Example JSON Response:
// Server sends this block of text
{
"id": 1,
"name": "MSMAXPRO",
"email": "contact@msmaxpro.me",
"address": {
"street": "123 Main St",
"city": "Tech City"
},
"skills": [ "Kotlin", "React", "Node.js" ]
}
HTTP Status Codes
When the server responds, it *always* includes a status code to tell you if your request was successful.
- 1xx (Informational): Request received, processing. (You rarely see this).
- 2xx (Success)
200 OK: The request was successful (e.g.,GET,PUT).
201 Created: A new resource was successfully created (e.g.,POST).
204 No Content: Success, but there's no data to send back (e.g.,DELETE).
- 3xx (Redirection)
301 Moved Permanently: The resource has a new URL.
- 4xx (Client Error) - **Your** app did something wrong.
400 Bad Request: You sent invalid data (e.g., a missing email).
401 Unauthorized: You are not logged in (missing or invalid API key/token).
403 Forbidden: You are logged in, but you don't have *permission* to see this data.
404 Not Found: The endpoint (URL) you asked for doesn't exist.
- 5xx (Server Error) - The **server** did something wrong.
500 Internal Server Error: The server's code crashed. There is nothing you can do but report the bug.
503 Service Unavailable: The server is down for maintenance or is overloaded.
Beyond REST: GraphQL and gRPC
While REST is the most common, you should be aware of two other major paradigms.
- GraphQL: Developed by Facebook. With REST, you always get the *full* data (over-fetching) or you have to make *multiple* requests (under-fetching). With GraphQL, the **client** specifies *exactly* which fields it wants in a single request (e.g., "just give me the `name` and `email` for user 123, and also their first 3 `posts`"). This is very efficient.
- gRPC: Developed by Google. This is a high-performance protocol that uses HTTP/2 and "Protocol Buffers" (which we saw in DataStore) instead of JSON. It's not human-readable but is *extremely* fast. It's used for high-performance microservices and real-time communication.
For most public APIs you'll consume, you will be using **REST with JSON**. We will focus on this.
The Android Networking "Stack"
To make HTTP requests in Android, you need a "networking client."
The Old Way: `HttpURLConnection`
This is the original, low-level library built into Java. You *can* use it, but you should *never* use it directly. It's verbose (requires tons of boilerplate code), hard to use, and doesn't support modern features easily. Most importantly, it's **synchronous**, meaning you have to manage threading yourself, which is a common source of bugs.
The Foundation: OkHttp
OkHttp is a third-party library by Square that has become the *de-facto* foundation for all modern networking in Android. It's an incredibly efficient, fast, and robust HTTP client. It automatically handles complex things like:
- HTTP/2 support
- Connection pooling (re-using connections to save time)
- Gzip compression (making requests smaller)
- Caching responses
- Handling network failures and retries
You can use OkHttp directly, but it's still a bit low-level. The *best* way is to use it implicitly with Retrofit.
The King: Retrofit
Retrofit** is the *industry standard* networking library for Android. It is also made by Square. Retrofit is a "type-safe REST client" that sits *on top* of OkHttp.
Its magic is that it lets you define your API as a simple **Kotlin interface**, and then Retrofit **writes all the networking code for you** at runtime. You just call a Kotlin function, and Retrofit handles the rest.
The Challenger: Ktor
Ktor Client is the modern, Kotlin-native alternative from JetBrains (creators of Kotlin). It's built from the ground up with coroutines and is designed for Kotlin Multiplatform. It's an excellent choice for new projects, but Retrofit is still more widely used in the industry.
We will cover **Retrofit (the standard)** first, then **Ktor (the modern alternative)**.
Deep Dive: Retrofit (The Standard Library)
Let's build a complete example of fetching data from a free public API. We will use **JSONPlaceholder** (https://jsonplaceholder.typicode.com/), which provides fake API data for testing.
Our goal: Fetch a list of User objects from /users, and create a new Post.
Step 1: Add Gradle Dependencies
We need three libraries:
- Retrofit: The main library.
- OkHttp: We add this for its `HttpLoggingInterceptor`.
- A JSON Converter: Retrofit fetches raw JSON text. We need a library to "parse" that JSON into our Kotlin data classes. We'll use **Moshi**, which is the modern, Kotlin-first choice.
In your build.gradle.kts (Module: :app) file:
// We'll use KSP for Moshi's code generation
plugins {
// ...
id("com.google.devtools.ksp")
}
dependencies {
// ... other dependencies
// --- Networking: Retrofit & OkHttp ---
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") // For logging
// --- JSON Parser: Moshi (Recommended) ---
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
}
Click **"Sync Now"**.
Step 2: Define your Data Classes (Models)
First, we need to look at the JSON response from the API. A single user from /users looks like this:
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz"
}
Now, we create a Kotlin data class that **exactly matches** these keys. We also add the Moshi annotation for code generation.
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// This tells Moshi to generate an adapter for this class
@JsonClass(generateAdapter = true)
data class User(
// @Json tells Moshi to match this property with the JSON key
@Json(name = "id") val id: Int,
@Json(name = "name") val name: String,
@Json(name = "username") val username: String,
@Json(name = "email") val email: String
)
// We'll also create a class for POST-ing a new post
@JsonClass(generateAdapter = true)
data class PostRequest(
@Json(name = "userId") val userId: Int,
@Json(name = "title") val title: String,
@Json(name = "body") val body: String
)
// And a class for the server's response
@JsonClass(generateAdapter = true)
data class PostResponse(
@Json(name = "id") val id: Int,
@Json(name = "userId") val userId: Int,
@Json(name = "title") val title: String,
@Json(name = "body") val body: String
)
Step 3: Create the API Interface (`ApiService.kt`)
This is the magic of Retrofit. You create a Kotlin interface and use **annotations** to describe your API endpoints. Retrofit will write the code for these functions *for you*.
import retrofit2.Response
import retrofit2.http.*
import okhttp3.MultipartBody
import okhttp3.RequestBody
interface ApiService {
// --- GET Requests ---
@GET("users")
suspend fun getUsers(): List<User>
@GET("users/{id}")
suspend fun getUserById(
@Path("id") userId: Int
): User
@GET("posts")
suspend fun getPostsByUser(
@Query("userId") userId: Int
): List<PostResponse>
// --- POST Request (sends JSON) ---
@POST("posts")
suspend fun createPost(
@Body postRequest: PostRequest
): Response<PostResponse> // Returns 201 Created
// --- POST Request (sends Form data) ---
@FormUrlEncoded
@POST("posts")
suspend fun createPostWithFields(
@Field("userId") userId: Int,
@Field("title") title: String
): Response<PostResponse>
// --- PUT Request (Full Update) ---
@PUT("posts/{id}")
suspend fun updatePost(
@Path("id") postId: Int,
@Body postRequest: PostRequest
): Response<PostResponse>
// --- PATCH Request (Partial Update) ---
@PATCH("posts/{id}")
suspend fun patchPost(
@Path("id") postId: Int,
@Body body: Map<String, Any> // e.g., mapOf("title" to "New Title")
): Response<PostResponse>
// --- DELETE Request ---
@DELETE("posts/{id}")
suspend fun deletePost(
@Path("id") postId: Int
): Response<Unit> // No content in response
// --- File Upload Request ---
@Multipart
@POST("upload")
suspend fun uploadProfilePicture(
// This is the file itself
@Part image: MultipartBody.Part,
// This is other data, like a user ID
@Part("userId") userId: RequestBody
): Response<Unit>
}
Step 4: Create the Retrofit Instance (Singleton)
Now we build the Retrofit object that implements our ApiService interface. We do this once and share it across the entire app.
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import java.util.concurrent.TimeUnit
import com.codewithmsmaxpro.BuildConfig // Auto-generated
// A singleton object
object RetrofitInstance {
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
// Create a Moshi instance for JSON parsing
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
// Create an OkHttp Logging Interceptor
private val loggingInterceptor = HttpLoggingInterceptor().apply {
// Only log in debug builds to avoid leaking data in production
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
// Create an OkHttp client and add the interceptor
private val httpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
// .addInterceptor(AuthInterceptor()) // (Add your auth interceptor here)
.connectTimeout(30, TimeUnit.SECONDS) // Set connect timeout
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout
.writeTimeout(30, TimeUnit.SECONDS) // Set write timeout
.build()
// Create the Retrofit instance using the Builder
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL) // 1. Set the base URL
.client(httpClient) // 2. Set the custom OkHttp client
.addConverterFactory(MoshiConverterFactory.create(moshi)) // 3. Add Moshi
.build()
// This is the magic: create an implementation of our ApiService interface
val api: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}
Step 5: Making the API Call (in a ViewModel)
Now, from our ViewModel, we can finally make the network call. It's clean, safe, and easy to read. We use viewModelScope.launch to call the suspend function on a background thread.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import android.util.Log
import java.io.IOException
import retrofit2.HttpException
class MyViewModel : ViewModel() {
private val apiService = RetrofitInstance.api
fun fetchAllUsers() {
viewModelScope.launch {
try {
val userList = apiService.getUsers()
Log.d("MyViewModel", "Success! Fetched ${userList.size} users.")
println(userList)
} catch (e: HttpException) {
Log.e("MyViewModel", "API Error (HTTP): ${e.code()} - ${e.message()}")
} catch (e: IOException) {
Log.e("MyViewModel", "Network Error (No Internet?): ${e.message}")
} catch (e: Exception) {
Log.e("MyViewModel", "Unknown Error: ${e.message}")
}
}
}
fun createNewPost() {
viewModelScope.launch {
try {
val newPost = PostRequest(
userId = 1,
title = "My New Post",
body = "This is the content."
)
val response = apiService.createPost(newPost)
if (response.isSuccessful() && response.code() == 201) {
Log.d("MyViewModel", "Post created! ID: ${response.body()?.id}")
} else {
Log.e("MyViewModel", "Error creating post: ${response.code()}")
}
} catch (e: Exception) {
Log.e("MyViewModel", "Network error: ${e.message}")
}
}
}
}
Deep Dive: JSON Parsers (Moshi vs. Gson vs. Kotlinx)
Retrofit only downloads the raw JSON text. A "Converter" is responsible for parsing that text into Kotlin objects. This is a critical step, and you have three main choices.
Option 1: Gson (The Old Standard)
Gson (by Google) was the standard for years. It's easy to set up (converter-gson) and works *okay*.
How it works: Gson uses **reflection** at runtime. It "inspects" your Kotlin class (User) and the JSON text and tries to match them up.
The Problem: Reflection is *slow* and can be unsafe with Kotlin's nullability. If the API sends "email": null but your User class has val email: String (non-nullable), Gson might crash your app with a NullPointerException or (worse) assign null to a non-null variable, which causes a crash later.
Option 2: Moshi (The Modern Standard)
Moshi (by Square, same as Retrofit) is the recommended parser for Kotlin projects.
How it works: Moshi uses **code generation** (via KSP). When you "Build" your project, KSP runs and *writes* a UserJsonAdapter.kt file for you. This adapter is a hard-coded, optimized parser specifically for your User class.
The Benefits:
- Fast: No reflection at runtime. It uses fast, pre-generated code.
- Kotlin-Aware: It understands Kotlin's non-null types. If the API sends
"email": nullfor a non-nullString, Moshi will throw a clearJsonDataException, which you can catch, instead of crashing your app with an NPE later. - Adapter Support: Easy to add custom adapters (e.g., for parsing
Dateobjects).
**Conclusion: Always use Moshi for new projects.**
Option 3: Kotlinx.serialization (The Native Way)
This is the "official" serialization library from JetBrains (creators of Kotlin). It's also excellent, fast, and uses a compiler plugin instead of KSP. It's the standard for Kotlin Multiplatform. Its only downside is that it's slightly more complex to set up with Retrofit, but it's a fantastic choice.
To use it, you replace Moshi with these dependencies:
plugins {
// ...
id("org.jetbrains.kotlin.plugin.serialization")
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
}
And your data class changes to use @Serializable:
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
@Serializable
data class User(
@SerialName("id") val id: Int,
@SerialName("name") val name: String,
@SerialName("email") val email: String
)
Advanced Networking: Interceptors, Auth & File Uploads
1. OkHttp Interceptors (Deep Dive)
An Interceptor is a "man-in-the-middle" that "intercepts" every single request *before* it goes to the internet. You can use them to modify requests, log data, or handle errors. There are two types:
- Application Interceptors: Added with
.addInterceptor(). These run *first*. They operate on your "logical" request. They are best for adding headers (like Auth) or logging. - Network Interceptors: Added with
.addNetworkInterceptor(). These run *last*, right before the request goes to the network. They are more powerful and can see "real" network data like redirects (status 3xx).
Example: Logging Interceptor (What we already built)
The HttpLoggingInterceptor is a pre-built interceptor. It logs the entire request (headers, body) and response (headers, body, time taken) to Logcat. This is *essential* for debugging.
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
Example: Authentication Interceptor (`AuthInterceptor.kt`)
This is the *best* way to add an API Key or Auth Token to every request.
data/network/AuthInterceptor.ktimport okhttp3.Interceptor
import okhttp3.Response
// (In a real app, you would inject your DataStore/AuthManager)
class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenProvider.getToken() // Get the latest token
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
if (token != null) {
requestBuilder.header("Authorization", "Bearer $token")
}
val request = requestBuilder.build()
return chain.proceed(request)
}
}
2. Handling Token Refresh with `Authenticator`
What happens if your JWT (Auth Token) expires? The server will send a 401 Unauthorized error. Your app will fail.
OkHttp has a powerful feature called Authenticator. This is a special interceptor that *only* runs *after* the server sends a 401 error. Its job is to try and "fix" the authentication and retry the request automatically.
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
class TokenAuthenticator(private val tokenProvider: TokenProvider) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
// 1. Check if we've already tried to refresh
if (response.request.header("Authorization") != null) {
return null // We already tried, give up
}
// 2. Use the refresh token to get a new auth token (This must be a *synchronous* call)
val newToken = tokenProvider.refreshTokenSynchronously()
return if (newToken != null) {
// 3. Build a new request with the *new* token
response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
} else {
null // No new token, give up
}
}
}
Then, you add this to your `OkHttpClient`:
private val httpClient = OkHttpClient.Builder()
//...
.authenticator(TokenAuthenticator(myTokenProvider)) // <-- ADD THIS LINE
.build()
3. Uploading Files with `@Multipart` (Deep Dive)
How do you upload an image (like a profile picture) along with other data (like a username)? You can't just put it in a JSON. The standard way is using a `multipart/form-data` request.
ApiService.kt (Added function)import okhttp3.MultipartBody
import okhttp3.RequestBody
interface ApiService {
// ... other functions ...
@Multipart
@POST("user/avatar")
suspend fun uploadProfilePicture(
// This is the file itself
@Part image: MultipartBody.Part,
// This is other data, like a user ID
@Part("userId") userId: RequestBody
): Response<Unit>
}
To call this, you need to convert your File or Uri into RequestBody and MultipartBody.Part objects. This is complex boilerplate code.
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import android.content.Context
import android.net.Uri
fun uploadImage(context: Context, imageUri: Uri, userId: Int) {
viewModelScope.launch {
try {
// 1. Create the RequestBody for the user ID (text data)
val userIdBody = userId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
// 2. Create the RequestBody for the image file
// We need to get the actual file content from the Uri
val inputStream = context.contentResolver.openInputStream(imageUri)
val fileBytes = inputStream?.readBytes()
if (fileBytes == null) {
Log.e("Upload", "Cannot read file")
return@launch
}
val imageRequestBody = fileBytes.toRequestBody(
context.contentResolver.getType(imageUri)?.toMediaTypeOrNull()
)
// 3. Create the "Part" for the file
val imagePart = MultipartBody.Part.createFormData(
"profile_picture", // This is the "name" the server expects
"user_image.jpg", // The filename the server will see
imageRequestBody
)
// 4. Make the call
apiService.uploadProfilePicture(imagePart, userIdBody)
Log.d("MyViewModel", "File uploaded successfully!")
} catch (e: Exception) {
Log.e("MyViewModel", "File upload failed: ${e.message}")
}
}
}
Part 6: The Modern Alternative: Ktor Client
Retrofit is fantastic, but it was built for Java first. **Ktor** is a networking library built by JetBrains from the ground up, *for Kotlin*. It's 100% Kotlin, uses coroutines natively, and is the standard for Kotlin Multiplatform (KMP) apps.
Step 1: Add Ktor Dependencies
To use Ktor, you need the core client, an "engine" (we'll use OkHttp as the engine for its reliability), and serialization plugins.
plugins {
// ...
id("org.jetbrains.kotlin.plugin.serialization")
}
dependencies {
// ...
val ktor_version = "2.3.6"
// Ktor Core
implementation("io.ktor:ktor-client-core:$ktor_version")
// The "engine" - we use OkHttp for its power
implementation("io.ktor:ktor-client-okhttp:$ktor_version")
// Content negotiation (for JSON)
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
// The JSON parser (using kotlinx.serialization)
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
// Logging
implementation("io.ktor:ktor-client-logging:$ktor_version")
}
Step 2: Create the Ktor Client (Singleton)
Ktor's setup is programmatic. You build a client and "install" features (plugins) into it.
data/network/KtorInstance.ktimport io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
object KtorInstance {
val client = HttpClient(OkHttp) {
// 1. Configure JSON (ContentNegotiation)
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true // Very useful!
})
}
// 2. Configure Logging
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.BODY
}
// 3. Default request settings (like an interceptor)
defaultRequest {
url("https://jsonplaceholder.typicode.com/")
header(HttpHeaders.ContentType, ContentType.Application.Json)
// header(HttpHeaders.Authorization, "Bearer $myToken")
}
}
}
Step 3: Make API call with Ktor
Ktor doesn't use interfaces. You call functions directly on the client. It feels more "Kotlin-native" and uses suspend functions for everything.
import io.ktor.client.call.*
import io.ktor.client.request.*
class MyViewModel : ViewModel() {
private val client = KtorInstance.client
fun fetchUsersWithKtor() {
viewModelScope.launch {
try {
// 1. Make the GET request
val userList = client.get("users").body<List<User>>()
Log.d("MyViewModel", "Ktor: Fetched ${userList.size} users.")
// 2. Make a POST request
val newPost = PostRequest(1, "Ktor Post", "This is cool.")
val response = client.post("posts") {
setBody(newPost)
}
Log.d("MyViewModel", "Ktor: Post status: ${response.status}")
} catch (e: Exception) {
Log.e("MyViewModel", "Ktor Error: ${e.message}")
}
}
}
}
Part 7: Loading Images from the Network
Retrofit and Ktor are only for fetching text/JSON. To load an *image* from a URL (e.g., https://example.com/image.png) and display it in your app, you need a dedicated **Image Loading Library**.
These libraries are highly optimized. They handle caching (on disk and in memory), transformations (like cropping to a circle), and placeholders automatically.
Option 1: Coil (The Modern Standard for Compose)
**Coil (Coroutine Image Loader)** is the recommended library for Jetpack Compose. It's Kotlin-first, built on coroutines, and very easy to use.
Step 1: Add Dependency
implementation("io.coil-kt:coil-compose:2.5.0")
Step 2: Use the `AsyncImage` Composable
import coil.compose.AsyncImage
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.layout.ContentScale
@Composable
fun MyNetworkImage(imageUrl: String) {
AsyncImage(
model = imageUrl,
contentDescription = "User Profile Picture",
modifier = Modifier
.size(100.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
// Show a placeholder while loading
placeholder = painterResource(R.drawable.placeholder_avatar),
// Show an error image if it fails to load
error = painterResource(R.drawable.error_avatar)
)
}
Read More about Coil →
Option 2: Glide (The Classic Standard for XML/Views)
Glide is the most popular and battle-tested image loading library for Android. It is extremely fast and reliable, especially for the XML View system.
Step 1: Add Dependencies (Glide also uses an annotation processor)
implementation("com.github.bumptech.glide:glide:4.16.0")
ksp("com.github.bumptech.glide:ksp:4.16.0")
Step 2: Use Glide (in XML/View system)
To use Glide, you just need an <ImageView> in your XML and a single line of Kotlin.
// In your XML layout:
<ImageView
android:id="@+id/my_image_view"
android:layout_width="100dp"
android:layout_height="100dp" />
// In your MainActivity.kt (inside onCreate)
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CircleCrop
val imageView = findViewById<ImageView>(R.id.my_image_view)
val imageUrl = "https://example.com/api/image.png"
Glide.with(this) // 'this' is the Context
.load(imageUrl)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error_image)
.transform(CircleCrop()) // Apply a transformation
.into(imageView)
Read More about Glide →