feat: add Ktor networking layer with ApiService and auth token holder

Add ApiConfig, AuthTokenHolder singleton, expect/actual HttpClient
engine factories (CIO for Android/Desktop, Darwin for iOS), and
ApiService with four GET endpoints. Bearer token from AuthTokenHolder
is injected as Authorization header on each request.

Adjusted for Ktor 3.5.1: HttpClientEngineFactory lives in
io.ktor.client.engine; the timeout plugin is HttpTimeout (package
io.ktor.client.plugins), not HttpTimeoutPlugin. Tests wrap suspend
calls in runTest and assert request URLs without a trailing '?'.
This commit is contained in:
2026-07-08 17:07:20 +02:00
parent a24297f821
commit b1661aaa4f
8 changed files with 210 additions and 0 deletions
@@ -0,0 +1,5 @@
package ch.parano.myice.network
object ApiConfig {
const val baseUrl = "https://myice.parano.ch"
}
@@ -0,0 +1,23 @@
package ch.parano.myice.network
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.HttpTimeout
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
expect fun createEngine(): HttpClientEngineFactory<HttpClientEngineConfig>
fun createHttpClient(): HttpClient {
return HttpClient(createEngine()) {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
install(HttpTimeout) {
connectTimeoutMillis = 10_000
requestTimeoutMillis = 10_000
}
}
}
@@ -0,0 +1,38 @@
package ch.parano.myice.network
import ch.parano.myice.models.Account
import ch.parano.myice.models.Event
import ch.parano.myice.models.EventDetail
import ch.parano.myice.models.UserInfo
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.http.HttpHeaders
class ApiService(private val client: HttpClient) {
private fun authHeader(): String? = AuthTokenHolder.token?.let { "Bearer $it" }
suspend fun getUserInfo(): UserInfo =
client.get("${ApiConfig.baseUrl}/userinfo") {
authHeader()?.let { header(HttpHeaders.Authorization, it) }
}.body()
suspend fun getAccounts(): List<Account> =
client.get("${ApiConfig.baseUrl}/accounts") {
authHeader()?.let { header(HttpHeaders.Authorization, it) }
}.body()
suspend fun getSchedule(account: String): List<Event> =
client.get("${ApiConfig.baseUrl}/schedule") {
url.parameters.append("account", account)
authHeader()?.let { header(HttpHeaders.Authorization, it) }
}.body()
suspend fun getGameDetail(gameId: String, account: String): EventDetail =
client.get("${ApiConfig.baseUrl}/game/$gameId") {
url.parameters.append("account", account)
authHeader()?.let { header(HttpHeaders.Authorization, it) }
}.body()
}
@@ -0,0 +1,5 @@
package ch.parano.myice.network
object AuthTokenHolder {
var token: String? = null
}