📚

KMM + KtorでAPIリクエスト

2022/07/27に公開

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で宣言、 androidMainiosMain でactualで実装という感じで実装していきます。

commonMain

ApiClientというファイルを作成して、

ApiClient
import io.ktor.client.*
import kotlinx.coroutines.CoroutineDispatcher

expect class ApiClient() {
    val client: HttpClient
    val dispatcher: CoroutineDispatcher
}

androidMain

ApiClientというファイルを作成して、

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というファイルを作成して、

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 というファイルを作成して、

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)

ContentView
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