Ktor Client Patterns
Modern HTTP client for Kotlin.
Client Setup
kotlin1val httpClient = HttpClient(OkHttp) { 2 // JSON serialization 3 install(ContentNegotiation) { 4 json(Json { 5 ignoreUnknownKeys = true 6 isLenient = true 7 prettyPrint = false 8 }) 9 } 10 11 // Timeouts 12 install(HttpTimeout) { 13 requestTimeoutMillis = 30_000 14 connectTimeoutMillis = 10_000 15 socketTimeoutMillis = 30_000 16 } 17 18 // Logging (debug only) 19 install(Logging) { 20 logger = Logger.ANDROID 21 level = if (BuildConfig.DEBUG) LogLevel.BODY else LogLevel.NONE 22 } 23 24 // Default request config 25 defaultRequest { 26 url("https://api.example.com") 27 contentType(ContentType.Application.Json) 28 } 29 30 // Auth 31 install(Auth) { 32 bearer { 33 loadTokens { 34 BearerTokens(tokenStorage.accessToken, tokenStorage.refreshToken) 35 } 36 refreshTokens { 37 val response = client.post("auth/refresh") { 38 setBody(RefreshRequest(tokenStorage.refreshToken)) 39 } 40 tokenStorage.save(response.body()) 41 BearerTokens(response.body<TokenResponse>().accessToken, response.body<TokenResponse>().refreshToken) 42 } 43 } 44 } 45}
API Definition
kotlin1class UserApi(private val client: HttpClient) { 2 3 suspend fun getUsers(): List<UserDto> { 4 return client.get("users").body() 5 } 6 7 suspend fun getUser(id: String): UserDto { 8 return client.get("users/$id").body() 9 } 10 11 suspend fun createUser(request: CreateUserRequest): UserDto { 12 return client.post("users") { 13 setBody(request) 14 }.body() 15 } 16 17 suspend fun updateUser(id: String, request: UpdateUserRequest): UserDto { 18 return client.put("users/$id") { 19 setBody(request) 20 }.body() 21 } 22 23 suspend fun deleteUser(id: String) { 24 client.delete("users/$id") 25 } 26}
Error Handling
kotlin1class ApiException( 2 val statusCode: Int, 3 override val message: String 4) : Exception(message) 5 6suspend inline fun <reified T> HttpClient.safeRequest( 7 block: HttpRequestBuilder.() -> Unit 8): Result<T> = runCatching { 9 val response = request(block) 10 11 if (response.status.isSuccess()) { 12 response.body<T>() 13 } else { 14 throw ApiException( 15 statusCode = response.status.value, 16 message = response.bodyAsText() 17 ) 18 } 19} 20 21// Usage 22class UserRepository(private val api: UserApi, private val client: HttpClient) { 23 suspend fun getUser(id: String): Result<User> { 24 return client.safeRequest<UserDto> { 25 url("users/$id") 26 method = HttpMethod.Get 27 }.map { it.toDomain() } 28 } 29}
DTOs and Mapping
kotlin1@Serializable 2data class UserDto( 3 val id: String, 4 val email: String, 5 @SerialName("first_name") 6 val firstName: String, 7 @SerialName("created_at") 8 val createdAt: String 9) 10 11fun UserDto.toDomain(): User = User( 12 id = id, 13 email = email, 14 name = firstName, 15 createdAt = Instant.parse(createdAt) 16)
Interceptors
kotlin1val client = HttpClient(OkHttp) { 2 // Request interceptor 3 install(HttpSend) { 4 intercept { request -> 5 request.headers.append("X-Client-Version", BuildConfig.VERSION_NAME) 6 execute(request) 7 } 8 } 9}
Certificate Pinning
kotlin1val client = HttpClient(OkHttp) { 2 engine { 3 config { 4 certificatePinner( 5 CertificatePinner.Builder() 6 .add("api.example.com", "sha256/AAAA...") 7 .add("api.example.com", "sha256/BBBB...") // Backup pin 8 .build() 9 ) 10 } 11 } 12}
Remember: Ktor is coroutine-first. Embrace suspend functions, handle errors properly.