Step 11 of 15
Ktor client setup, GET/POST requests, kotlinx.serialization, error handling
Networking ด้วย Ktor — GET/POST, JSON serialization, error handling
Ktor is a coroutine-based HTTP client. It works well with Kotlin Multiplatform and integrates smoothly with kotlinx.serialization. Current version: 3.1.0.
// build.gradle.kts
plugins {
alias(libs.plugins.kotlin.serialization)
}
dependencies {
implementation("io.ktor:ktor-client-core:3.1.0")
implementation("io.ktor:ktor-client-cio:3.1.0")
implementation("io.ktor:ktor-client-content-negotiation:3.1.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
}
The kotlinx.serialization plugin generates serializers for @Serializable classes.
import kotlinx.serialization.Serializable
@Serializable
data class Post(
val id: Int,
val title: String,
val body: String
)
Configure the client once. Install ContentNegotiation with JSON so Ktor serializes and deserializes automatically.
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
val httpClient = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = false
})
}
}
ignoreUnknownKeys = true means extra fields in the JSON are skipped, not crashed on.
import io.ktor.client.call.*
import io.ktor.client.request.*
suspend fun fetchPost(id: Int): Post {
return httpClient.get("https://jsonplaceholder.typicode.com/posts/$id")
.body()
}
Call it from a coroutine:
suspend fun main() {
val post = fetchPost(1)
println(post)
}
// Output:
// Post(id=1, title=sunt aut facere repellat provident..., body=quia et suscipit...)
suspend fun createPost(title: String, body: String): Post {
return httpClient.post("https://jsonplaceholder.typicode.com/posts") {
contentType(ContentType.Application.Json)
setBody(Post(id = 0, title = title, body = body))
}.body()
}
interface PostRepository {
suspend fun getPost(id: Int): Post
suspend fun create(title: String, body: String): Post
}
class KtorPostRepository(private val client: HttpClient) : PostRepository {
override suspend fun getPost(id: Int): Post {
return client.get("https://jsonplaceholder.typicode.com/posts/$id").body()
}
override suspend fun create(title: String, body: String): Post {
return client.post("https://jsonplaceholder.typicode.com/posts") {
contentType(ContentType.Application.Json)
setBody(Post(id = 0, title = title, body = body))
}.body()
}
}
Wrap calls in a Result so callers do not crash on network errors:
suspend fun safeGetPost(id: Int): Result<Post> = runCatching {
client.get("https://jsonplaceholder.typicode.com/posts/$id").body()
}
suspend fun demo() {
safeGetPost(1)
.onSuccess { println("Loaded: ${it.title}") }
.onFailure { println("Failed: ${it.message}") }
}
// Success:
// Loaded: sunt aut facere repellat provident...
// Failure (no network):
// Failed: Connection refused
Map common HTTP errors to your own sealed type for cleaner UI handling:
sealed interface ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>
data class Error(val message: String) : ApiResult<Nothing>
data object Loading : ApiResult<Nothing>
}
In AndroidManifest.xml, add the internet permission:
<uses-permission android:name="android.permission.INTERNET" />
Without this, every request fails on Android.
Create a UserRepository that fetches a user from https://jsonplaceholder.typicode.com/users/{id}. Use Result for error handling. Call it from a UserViewModel and expose the result as StateFlow<ApiResult<User>>. Test with airplane mode on to confirm the error path works.