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ăng | Ktor | Retrofit |
|---|---|---|
| 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ần | Mô tả |
|---|---|
HttpClient | Client chính để gọi API |
ContentNegotiation | Plugin xử lý JSON |
@Serializable | Annotate data classes |
get/post/put/delete | HTTP methods |
body() | Parse response thành object |
parameter() | Query parameters |
setBody() | Request body |
Best Practices
- Dùng expect/actual cho HttpClient engine
- Centralize error handling với
safeApiCall - Dùng sealed class cho UI states
- 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