KMM + KtorでAPIリクエスト
KMM + KtorでAPIリクエストするにはどうしたらいいかググっていて、検索にヒットした記事の内容が現在のバージョンに対応していなかったので記事として残しておきます。
※KMMでは両OSのライブラリも使えるようなので、必ずしもKtorを使う必要はなさそうです。
こちらのページを参考に実装していきます。
Creating a cross-platform mobile application
Content negotiation and serialization
依存関係を追加
shared/build.gradle.kts
を開いて sourceSets の中身をいじっていきます。
<-- 省略 -->
sourceSets {
val ktorVersion = "2.0.3"
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-json:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
}
}
<-- 省略 -->
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
}
}
<-- 省略 -->
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
}
}
}
<-- 省略 -->
HttpClientの実装
続いて、APIリクエストで使うHttpClientを実装します。
/sharedの、 commonMain
でexpectで宣言、 androidMain
と iosMain
でactualで実装という感じで実装していきます。
commonMain
ApiClientというファイルを作成して、
import io.ktor.client.*
import kotlinx.coroutines.CoroutineDispatcher
expect class ApiClient() {
val client: HttpClient
val dispatcher: CoroutineDispatcher
}
androidMain
ApiClientというファイルを作成して、
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
actual class ApiClient actual constructor() {
actual val client: HttpClient = HttpClient(OkHttp) {
install(ContentNegotiation) {
json(json = kotlinx.serialization.json.Json {
isLenient = false
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = false
})
}
}
actual val dispatcher: CoroutineDispatcher = Dispatchers.Default
}
iosMain
ApiClientというファイルを作成して、
import io.ktor.client.*
import io.ktor.client.engine.darwin.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Runnable
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue
import platform.darwin.dispatch_queue_t
import kotlin.coroutines.CoroutineContext
actual class ApiClient actual constructor() {
actual val client: HttpClient = HttpClient(Darwin) {
install(ContentNegotiation) {
json(json = kotlinx.serialization.json.Json {
isLenient = false
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = false
})
}
}
actual val dispatcher: CoroutineDispatcher = Dispatcher(dispatch_get_main_queue())
}
/// iOSの場合はそのままではコルーチンが使えないので以下のようにdispatch_queueを使って対応します。
class Dispatcher(private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatchQueue) {
block.run()
}
}
}
GithubAPIにリクエストしてみる
上記で実装したApiClientを使って実際にGithubAPIにリクエストしてリポジトリの一覧を取得してみます。
/shared/commonMain
に GithubApi というファイルを作成して、
import io.ktor.client.call.*
import io.ktor.client.request.*
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Repository(
val name: String,
@SerialName("html_url") val url: String,
)
class GithubApi {
companion object {
const val BASE_URL = "https://api.github.com"
}
private val apiClient = ApiClient()
@OptIn(DelicateCoroutinesApi::class)
suspend fun fetchRepos(): List<Repository> {
val result = apiClient.client.get("$BASE_URL/users/{ユーザーID}/repos")
return result.body()
}
}
あとはView側でfetchRepos()を読んで表示してあげれば良いだけです。
Listで表示する例(iOS)
import SwiftUI
import shared
struct ContentView: View {
let githubApi = GithubApi()
@State var greet = "Loading"
@State var repos: [Repository] = []
var body: some View {
VStack {
Text(greet)
List(repos, id: \.self) { repo in
Text(repo.name)
}
}
.onAppear(perform: load)
}
func load() {
githubApi.fetchRepos { result, error in
if let result = result {
repos = result
greet = "Success"
} else if let error = error {
greet = "Error: \(error)"
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
まとめ
最近バージョンのKMM + KtorでAPIリクエストする実装についてまとめました。
Kotlinをしばらく触っていないので、正直もう少し上手くかける気がするのですが、この記事のメインはKtorなので目を瞑ってください。
今回の例ではKMMのメリットを享受できてない感があるので今後もキャッチアップして記事にしていこうと思います。
Discussion