見出し画像

KMP+OpenAPIで始めるAPI開発:技術検証の第一歩(環境構築編)


はじめに

株式会社プログリットでエンジニアのインターンをしているKaiです。現在、プログリットではモバイルアプリ開発にKotlin Multiplatform(以後KMP)の導入を検討しております。そこで、KMPモジュールにOpenAPIクライアントコードを実装するまでの環境構築についてまとめます。

KMP導入理由については下記の記事で述べています。

当記事の対象者

KMP + OpenAPIでアプリ開発を行いたい方!

KMPディレクトリ構成

技術検証で扱うプロジェクト構成です。

KMPディレクトリ構成
  • kmp_Android_app:Androidプロジェクト

  • kmp_iOS_app:iOSプロジェクト

  • kmp_module:KMPモジュール(実際にKMPの処理を書く場所)

KMPの処理内容はkmp_module内にあるsharedフォルダ内に記述していきます。

kmp_moduleフォルダ構成

OpenAPIクライアントコードの生成

まずは、APIが定義されたswaggerファイルをKMPのルートディレクトリ配下に作成します。

次に、OpenAPI generatorを使用してクライアントコードを生成します

openapi-generator generate -i path/to/swagger/swagger.json -g kotlin -o path/to/project/shared/src/commonMain/kotlin/com/example/kmpposapp/openapi -c api_client_config.yml --additional properties=library=multiplatform,dateLibrary=kotlinx-datetime,testLibrary=kotest

※ここで気を付けなければいけないのは、クライアントコードを/shared内に生成しなければいけないということです。
KMPのコードを/shared内に書かないとIDEが認識してくれないためです。

  • -i :swaggerファイルのpath指定

  • -g:クライアントコードの言語指定

  • -o:生成後のクライアントコードのpath指定

  • -c:configファイルの指定。生成されるクライアントコードのパッケージ  名などを設定

--additional-properties=library=multiplatform,dateLibrary=kotlinx-datetime,testLibrary=kotest

`--additional-properties`だけ別で追加の説明をします。
ここではクライアントコードを生成する時に扱うライブラリを指定しています。今回の技術検証では以下のライブラリを使用しています。

  • ネットワーク通信:Ktor(version 2.3.11)

  • シリアライゼーション:kotlinx-serialization-json(version 1.7.0)

  • DateTime:kotlinx-datetime(version 0.4.1)

  • テスト:kotest(version 5.9.1)

`--additional-properties`に`dateLibrary`を指定している理由としては、クライアントコードを生成する時に、DateTime関連のエラーが起きたためです。
(詳細なエラーの内容を記録するのを忘れてしまいました🙇)

上記以外にも`--additional-properties`で設定オプションが存在し、生成されるデータクラス名の変更も行うことができます。

生成コマンドを実行すると以下のようにクライアントコードが生成されます(openapiフォルダ)

openapiフォルダ構成

srcの中身は以下のようになっています。

src中身
  • apis:swaggerで定義された各種Api

  • auth:認証関連の処理

  • dto:各DTO(error, request, response..etc)

  • infrastructure:ApiClient, RequestConfig, serialization ..etc

開発でよく使う生成クライアントコードを簡単に紹介してみます。

ApiClient.kt

open class ApiClient(
        private val baseUrl: String
) {

    private lateinit var client: HttpClient

    constructor(
        baseUrl: String,
        httpClientEngine: HttpClientEngine?,
        httpClientConfig: ((HttpClientConfig<*>) -> Unit)? = null,
        jsonBlock: Json
    ) : this(baseUrl = baseUrl) {
        val clientConfig: (HttpClientConfig<*>) -> Unit by lazy {
            {
                it.install(ContentNegotiation) { json(jsonBlock) }
                it.install(Auth) {
                    bearer {
                        loadTokens {
                            BearerTokens(TokenProvider.getBearerToken(), "")
                        }
                        refreshTokens {
                            BearerTokens(TokenProvider.getRefreshToken(), "")
                        }
                    }
                }
                httpClientConfig?.invoke(it)
            }
        }

        client = httpClientEngine?.let { HttpClient(it, clientConfig) } ?: HttpClient(clientConfig)


    }

    constructor(
        baseUrl: String,
        httpClient: HttpClient
    ): this(baseUrl = baseUrl) {
        this.client = httpClient
    }

    private val authentications: kotlin.collections.Map<String, Authentication> by lazy {
        mapOf(
            "bearerAuth" to HttpBearerAuth("bearer")
        )
    }

    companion object {
        const val BASE_URL = ""
        val JSON_DEFAULT = Json {
          ignoreUnknownKeys = true
          prettyPrint = true
          isLenient = true
        }
        protected val UNSAFE_HEADERS = listOf(HttpHeaders.ContentType)
    }

この`ApiClient`クラスは全てのApiクラスの基盤となるクラスになっています。

今回、openapi generatorコマンドで、サーバー通信ライブラリにKtorを指定しているために、Ktorを用いたクライアントコードが生成されます。

(Retrofitも指定できるみたいですが、KtorはKMPに特化したHttpクライントであるためにKtorを使用することをお勧めします。)

StudentApi.kt

Swaggerで定義された、とあるApi(一部抜粋)のクライアントコードになります。

open class HogeApi : ApiClient {

        constructor(
        baseUrl: String = ...,
        httpClientEngine: HttpClientEngine? = null,
        httpClientConfig: ((HttpClientConfig<*>) -> Unit)? = null,
        jsonSerializer: Json = ...
    ) : super(baseUrl = baseUrl, httpClientEngine = httpClientEngine, httpClientConfig = httpClientConfig, jsonBlock = jsonSerializer)

    constructor(
        baseUrl: String,
        httpClient: HttpClient
    ): super(baseUrl = baseUrl, httpClient = httpClient)

    open suspend fun hoge(): HttpResponse<HogeDTO> {
        val localVariableAuthNames = ...
        val localVariableBody = ...
        val localVariableQuery = ...
        val localVariableHeaders = ...
        val localVariableConfig = ...

        return request(
            localVariableConfig,
            localVariableBody,
            localVariableAuthNames
        ).wrap()
    }
}

各Apiクラス内でHttpメソッドの種類やリクエストConfigの設定を行い、`ApiClient`クラス内にある`request`メソッドに渡してあげて、`ApiClient`クラスがサーバーにリクエストを送る、という流れになっています。

また、親クラスである`ApiClient`の`request`メソッドを継承して、各Apiクラスからサーバーにリクエストを送るという流れとなっています。

以下、`ApiCient`の`request`メソッド

protected suspend fun <T: Any?> request(requestConfig: RequestConfig<T>, body: Any? = null, authNames: kotlin.collections.List<String>): HttpResponse {
    requestConfig.updateForAuth<T>(authNames)
    val headers = requestConfig.headers

    return client.request {
        this.url {
            this.takeFrom(URLBuilder(baseUrl))
            appendPath(requestConfig.path.trimStart('/').split('/'))
            requestConfig.query.forEach { query ->
                query.value.forEach { value ->
                    parameter(query.key, value)
                }
            }
        }
        this.method = requestConfig.method.httpMethod
        headers.filter { header -> !UNSAFE_HEADERS.contains(header.key) }.forEach { header -> this.header(header.key, header.value) }
        if (requestConfig.method in listOf(RequestMethod.PUT, RequestMethod.POST, RequestMethod.PATCH)) {
            val contentType = (requestConfig.headers[HttpHeaders.ContentType]?.let { ContentType.parse(it) }
                ?: ContentType.Application.Json)
            this.contentType(contentType)
            this.setBody(body)
        }
    }
}

KMPで定義したメソッドを別プロジェクトのiOS/Androidから呼び出す方法

KMPでモジュールを作成し、iOSおよびAndroidにモジュールを設定してあげることでKMPのコードを呼び出すことができます。

iOSモジュール生成〜設定まで

以下コマンドをkmp_moduleのルートで実行します。

※KMPはArm64 Platformにしか対応していないため、Arm64を指定する必要があります。

./gradlew :shared:linkDebugFrameworkIosArm64

コマンドを実行すると、Arm64のモジュールが./shared/build/bin配下に生成されます。

Arm64モジュール

Xcodeに移り、iOSのモジュールの設定を行っていきます。

次に、生成されたshared.frameworkをBuild PhasesタブのEmbed Frameworks及び、GeneralタブのFrameworks, Libraries, and Embed Contentに設定します。

GeneralタブのFrameworks, Libraries, and Embed

さらに、Kotlinで書かれたコードをSwiftに変換するためのHeaderファイルを作成します。

Headerファイル

ファイル内でimportするのは以下2つになります。

  1.  生成されたiOSモジュール内のshared.hファイル(sharedはモジュール名であり、各自で好きな名前に変更できます。)

  2.  KMPモジュール内にある、sharedフォルダ

  • #import :特定のヘッダーファイルをインクルードするために使用されます。shared.hのようなファイルは、Kotlin/Nativeで生成されたものであり、具体的なパスを指定してそのファイルを取り込む必要があります。

  • @import :モジュール全体をインポートするために使用されます。これにより、モジュール内のすべての関数やクラスなどを利用することができます。

最後に、作成したHeaderファイルをSwift Compilerに設定します。

Swift Compiler

Androidモジュール生成~設定まで

以下コマンドをkmp_moduleのルートで実行します。

./gradlew :shared:assembleDebug
./gradlew :shared:assembleRelease

コマンドを実行すると、debug及びreleaseのモジュールが./shared/build/outputs/aar配下に生成されます。

aarファイル格納箇所

次に、Androidプロジェクトのbuild.gradle.kts(:app)内にKMPモジュールの依存関係を設定します。

依存関係設定

(モジュールのpathは、モジュールを管理している各自のローカルpathを参照してください。)

Androidの設定は以上になります!簡単!

上記の設定を終えると、iOS/Android各プロジェクトからKMPで定義した関数やクラスを呼べるようになります。

まとめ

上記の設定を終えると、KMPモジュールを各iOS/Androidアプリから呼べるようになります。

次回はKMP+OpenAPIの具体的な実装コードについて述べていきたいと思います。


プログリットの成長を加速させる仲間を募集しています!

プログリットでは、プロダクト開発のメンバーを募集しています!
「世界で自由に活躍できる人を増やす」というミッションに共感してくださる方、組織の中でお互いに切磋琢磨しながら成長していきたいという方は、ぜひカジュアル面談でお話しましょう!



この記事が気に入ったらサポートをしてみませんか?