Skip to Content

Ktor - Networking trong KMP

Ktor là thư viện HTTP client/server của JetBrains, hỗ trợ hoàn toàn Kotlin Multiplatform. Đây là lựa chọn phổ biến nhất cho networking trong KMP.

Tại sao dùng Ktor?

Tính năngKtorRetrofit
Multiplatform✅ Có❌ Android only
Kotlin-first✅ 100% Kotlin⚠️ Java-based
Coroutines✅ Native support⚠️ Cần adapter
Serialization✅ Kotlin Serialization⚠️ Gson/Moshi

Bước 1: Thêm Dependencies

libs.versions.toml

[versions] ktor = "2.3.7" kotlinx-serialization = "1.6.2" [libraries] # Ktor client core ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } # Platform-specific engines ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } # Serialization kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } [plugins] kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

shared/build.gradle.kts

plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinx.serialization) // Thêm plugin } kotlin { sourceSets { commonMain.dependencies { implementation(libs.ktor.client.core) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.client.logging) implementation(libs.kotlinx.serialization.json) } androidMain.dependencies { implementation(libs.ktor.client.okhttp) // OkHttp engine cho Android } iosMain.dependencies { implementation(libs.ktor.client.darwin) // Darwin engine cho iOS } } }

Bước 2: Tạo HTTP Client

commonMain/network/HttpClientFactory.kt

import io.ktor.client.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.logging.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json // expect để mỗi platform chọn engine phù hợp expect fun createPlatformHttpClient(): HttpClient // Cấu hình chung fun createHttpClient(): HttpClient { return createPlatformHttpClient().config { // JSON Serialization install(ContentNegotiation) { json(Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true // Bỏ qua field không khai báo }) } // Logging (debug) install(Logging) { logger = Logger.DEFAULT level = LogLevel.BODY } } }

androidMain/network/HttpClientFactory.android.kt

import io.ktor.client.* import io.ktor.client.engine.okhttp.* actual fun createPlatformHttpClient(): HttpClient = HttpClient(OkHttp)

iosMain/network/HttpClientFactory.ios.kt

import io.ktor.client.* import io.ktor.client.engine.darwin.* actual fun createPlatformHttpClient(): HttpClient = HttpClient(Darwin)

Bước 3: Định nghĩa Data Models

// commonMain/data/model/User.kt import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName @Serializable data class User( val id: Int, val name: String, val email: String, @SerialName("avatar_url") // Map từ snake_case sang camelCase val avatarUrl: String? = null ) @Serializable data class UsersResponse( val data: List<User>, val page: Int, @SerialName("total_pages") val totalPages: Int )

Bước 4: Tạo API Service

// commonMain/data/network/UserApiService.kt import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.http.* class UserApiService(private val httpClient: HttpClient) { private val baseUrl = "https://reqres.in/api" // GET request - Lấy danh sách users suspend fun getUsers(page: Int = 1): UsersResponse { return httpClient.get("$baseUrl/users") { parameter("page", page) }.body() } // GET request - Lấy user theo ID suspend fun getUserById(id: Int): User { val response: UserDetailResponse = httpClient.get("$baseUrl/users/$id").body() return response.data } // POST request - Tạo user mới suspend fun createUser(name: String, job: String): CreateUserResponse { return httpClient.post("$baseUrl/users") { contentType(ContentType.Application.Json) setBody(CreateUserRequest(name, job)) }.body() } // PUT request - Cập nhật user suspend fun updateUser(id: Int, name: String, job: String): UpdateUserResponse { return httpClient.put("$baseUrl/users/$id") { contentType(ContentType.Application.Json) setBody(CreateUserRequest(name, job)) }.body() } // DELETE request suspend fun deleteUser(id: Int): Boolean { val response = httpClient.delete("$baseUrl/users/$id") return response.status == HttpStatusCode.NoContent } } // Request/Response models @Serializable data class CreateUserRequest(val name: String, val job: String) @Serializable data class CreateUserResponse( val id: String, val name: String, val job: String, val createdAt: String ) @Serializable data class UpdateUserResponse( val name: String, val job: String, val updatedAt: String ) @Serializable data class UserDetailResponse(val data: User)

Bước 5: Xử lý lỗi

// commonMain/data/network/NetworkResult.kt sealed class NetworkResult<out T> { data class Success<T>(val data: T) : NetworkResult<T>() data class Error(val exception: Throwable, val message: String) : NetworkResult<Nothing>() object Loading : NetworkResult<Nothing>() } // commonMain/data/network/SafeApiCall.kt import io.ktor.client.plugins.* import io.ktor.utils.io.errors.* suspend fun <T> safeApiCall(apiCall: suspend () -> T): NetworkResult<T> { return try { NetworkResult.Success(apiCall()) } catch (e: ClientRequestException) { // 4xx errors NetworkResult.Error(e, "Client error: ${e.response.status}") } catch (e: ServerResponseException) { // 5xx errors NetworkResult.Error(e, "Server error: ${e.response.status}") } catch (e: IOException) { // Network errors NetworkResult.Error(e, "Network error: Check your connection") } catch (e: Exception) { NetworkResult.Error(e, "Unknown error: ${e.message}") } }

Sử dụng:

class UserRepository(private val apiService: UserApiService) { suspend fun getUsers(page: Int): NetworkResult<List<User>> { return safeApiCall { apiService.getUsers(page).data } } }

Bước 6: Sử dụng với ViewModel

// commonMain/presentation/UserViewModel.kt import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class UserViewModel(private val repository: UserRepository) { private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading) val uiState: StateFlow<UserUiState> = _uiState.asStateFlow() suspend fun loadUsers() { _uiState.value = UserUiState.Loading when (val result = repository.getUsers(1)) { is NetworkResult.Success -> { _uiState.value = UserUiState.Success(result.data) } is NetworkResult.Error -> { _uiState.value = UserUiState.Error(result.message) } is NetworkResult.Loading -> { /* handled above */ } } } } sealed class UserUiState { object Loading : UserUiState() data class Success(val users: List<User>) : UserUiState() data class Error(val message: String) : UserUiState() }

Bước 7: Sử dụng trong Compose UI

// commonMain/ui/screen/UserListScreen.kt @Composable fun UserListScreen(viewModel: UserViewModel) { val uiState by viewModel.uiState.collectAsState() LaunchedEffect(Unit) { viewModel.loadUsers() } when (val state = uiState) { is UserUiState.Loading -> { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } is UserUiState.Success -> { LazyColumn { items(state.users) { user -> UserRow(user) } } } is UserUiState.Error -> { Column( Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("Error: ${state.message}") Button(onClick = { /* retry */ }) { Text("Retry") } } } } } @Composable fun UserRow(user: User) { Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { // Avatar - dùng Coil/Kamel cho image loading Box( modifier = Modifier .size(48.dp) .background(Color.Gray, CircleShape) ) Spacer(Modifier.width(16.dp)) Column { Text(user.name, fontWeight = FontWeight.Bold) Text(user.email, color = Color.Gray) } } }

📝 Tóm tắt

Thành phầnMô tả
HttpClientClient chính để gọi API
ContentNegotiationPlugin xử lý JSON
@SerializableAnnotate data classes
get/post/put/deleteHTTP methods
body()Parse response thành object
parameter()Query parameters
setBody()Request body

Best Practices

  1. Dùng expect/actual cho HttpClient engine
  2. Centralize error handling với safeApiCall
  3. Dùng sealed class cho UI states
  4. Inject HttpClient qua DI (Koin)

Tiếp theo

Học về Kotlin Serialization để hiểu sâu hơn về JSON parsing.

Last updated on