見出し画像

KMP+OpenAPIで始めるAPI開発:技術検証の第二歩(課題解消編)


はじめに

株式会社プログリットでエンジニアのインターンをしているKaiです。現在、プログリットではモバイルアプリ開発にKotlin Multiplatform(以下KMP)の導入を検討しております。そこで、導入の前にまずは技術検証から行おうということで、これまで地道に検証を行ってきました。当記事では、技術検証を通して得られた知見をまとめます。

当記事の対象者

・KMP + OpenAPIでアプリ開発を行いたい方
・iOSとAndroidのAPI基盤(UseCase/ Repository)を一つにまとめたい方
・なんとなくKMPに興味がある方

KMP導入検討理由

プログリットはモバイルアプリを複数展開しており、改めて現状のモジュール構造を見るとAPIを重複実装している箇所が拝見されました。

下図が現状のモジュール構造です。

モジュール構造(現在)

このモジュール構造の場合、青色の図で示されている認証関連のAPIを各アプリから呼び出していることになります。そして、今後も新たなアプリが追加されていくことを考えると、似たような処理を何度も実装することになり開発の効率が低下するのではないかという結論に至りました。

そこで、「認証関連のUseCaseクラスやRepositoryなどのAPIアクセスをKMPで共通化」し、冗長性を排除すべくKMPの導入を検討しています。

下図がKMPを導入した場合のモジュール構造です。

モジュール構造(展望)

各アプリからUseCaseを切り出し、KMPで実装するようにします。

`KMP Jobs`という青色の図が、認証関連のOpenAPIクライアントコードを用いてUseCaseまでの実装を行います。その後、各アプリからこのUseCaseを呼び出してviewModelで扱うという流れとなります。

浮かんだ懸念点

それは、「各アプリのUseCaseに、アプリ固有の処理の実装が必要になる」ということです。

ローカルDBやログアウト後のトークンの処理などが各アプリによって異なるために、完全に1つのUseCaseに共通化させることが困難であるものが存在します。

そのため、そういったものは「Repositoryまでを共通化させる」という方針に決定しました。

このRepositoryが`KMP Jobs`という青色の図に当たり、これを各UseCaseから呼び出すという想定です。

技術検証の目的

「UseCaseクラスやRepositoryなどのAPIアクセスをKMPで共通化する」という構造が実現可能なのかを動くコードで検証し、「懸念点として挙げられる課題を解消すること」が技術検証の目的です。

具体的な課題は以下の3つです。

  1. DataSourceを最終的にsuspend関数を提供するUseCaseクラスとして機能させる。

  2. OpenAPI Generatorで生成されたコードをRemote DataSourceとして実装する。(弊社ではSwaggerを使用してコードを自動生成し、既存の業務を効率化しています。)

  3. iOS側からsuspend関数を提供するUseCaseを複雑なボイラープレートコードやブリッジ実装無しにシームレスに呼び出す。(シンプルで効率的な呼び出し方法を確立する)

OpenAPIクライアントコードをRemote DataSource及び、UseCaseクラスとして機能させる

KMPの導入にあたり想定される課題1,2(上記参照)を解消していきます。

期待結果としては、「OpenAPIクライアントコードをクリーンアーキテクチャに沿ってUseCaseまで実装し、かつAPI疎通が取れること」です。

では検証していきます。

※具体的なクライアントコードは以下の記事で紹介しています。

いきなりコード載せちゃいます!

ドメイン層

interface HogeRepository {
    suspend fun hogeGetHoge(): KmmResult<Hoge>
}

class HogeRepositoryImpl: HogeRepository {
    private val hogeAPI: HogeAPI = HogeAPI()

    override suspend fun hogeGetHoge(): KmmResult<Hoge> {
        return try {
            val result = hogeAPI.getHoge().toDomain()
            KmmResult.success(result)
        } catch (e: Exception) {
            println(e)
            KmmResult.failure(e)
        }
    }
}

アプリケーション層

class HogeGetUseCase: HogeGetUseCaseInterface {

    private val repository: HogeRepository = HogeRepositoryImpl()

    override suspend fun execute(): KmmResult<Hoge> = repository.hogeGetHoge()
}

コンパイルエラーは特になく、suspend関数を提供するUseCaseまでの実装が完了しました。

これで課題1が解消できました!!

次に、このUseCaseをiOS/Androidから呼び出してAPI疎通が取れるか確認します。

結果的に、無事にAPI疎通が取れることが確認できました。(具体的な処理結果は省きます)

これで課題2が解消できました!!

まとめると、OpenAPIクライアントコードをRemote DataSource及び、UseCaseクラスとして問題なく機能させることができました。

iOS側からUseCaseを複雑なボイラープレートコードやブリッジ実装無しにシームレスに呼び出す

KMPの導入にあたり想定される課題3(上記参照)を解消していきます。

Kotlinのsuspend関数とSwiftのconcurrency Taskが問題なくシームレスに変換できるかを検証しました。

以下Kotlinのsuspend関数を呼び出しているSwiftコードの箇所です。

@MainActor
class HogeViewModel: ObservableObject {
    @Published var uiState: HogeUiState = .initial
    
    private let hogeGetUseCase: HogeGetUseCaseInterface
    
    init() {
        self.hogeGetUseCase = HogeGetUseCase()
    }

    func getHoge() {
        Task {
            uiState.isLoading = true
            do {
                let result = try await hogeGetUseCase.execute().getOrThrow()
                uiState.hogeInfo = result
            } catch {
                print(error)
                uiState.error = error.localizedDescription
            }
            uiState.isLoading = false
        }
    }
}

結論としては、Kotlinのsuspend関数とSwiftのconcurrency Taskを問題なくシームレスに変換できました。

これで技術検証の課題3が解消できました!!

問題なくKMP+OpenAPIの実装ができそうですね。

suspendとconcurrencyの相互変換プロセスについて

「どうやって相互変換しているんだろう」
この疑問を解消させるために調べてみました。

結論として、`Kotlin/Native`が自動的に生成するObjective-C/Swiftインターフェースによって、`WithCompletionHandler`のような形式に変換されています。そして、この変換によりKotlinの非同期処理(suspend関数)をSwiftの`async/await`や`completionHandler`としてシームレスに呼び出すことが出来ます。

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("HogeGetUseCase")))
@interface SharedHogeGetUseCase : SharedBase <SharedHogeGetUseCaseInterface>
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
/**
* @note This method converts instances of CancellationException to errors.
* Other uncaught Kotlin exceptions are fatal.
*/
- (void)executeWithCompletionHandler:(void (^)(SharedKmmresultKmmResult<SharedStudent *> * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("execute(completionHandler:)")));
@end

`HogeGetUseCase`の`execute()`が`executeWithCompletionHandler`という関数に変換されて実装されていることがわかります。これでKotlinのsuspend関数をSwiftのconcurrencyに変換させているのだと思います。

まとめ

KMPとOpenAPIの組み合わせに関する情報が少なく、技術検証はとても大変でした…。

また、プログリットではKMPのように課題解決につながる新しい技術の導入に前向きな姿勢が見られ、「新たな技術に触れてみたい!」という方にぴったりの環境だと感じました。

次回は、KMPのライブラリであるKmmResultについて述べたいと思います。


余談

/**
* @note This method converts instances of CancellationException to errors.
* Other uncaught Kotlin exceptions are fatal.
*/
- (void)executeWithCompletionHandler:(void (^)(SharedKmmresultKmmResult<SharedStudent *> * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("execute(completionHandler:)")));
@end

Objective-C/Swiftインターフェースに書かれている"SharedKmmresultKmmResult"は、KmmResultというKMPのフレームワークです。

⚠️本来は"SharedKmmResult"という名前が適切ですが、フレームワークの処理上、コード生成の段階で名前が重複するという仕様になっています。   (今度issueを投げて改善を提案してみようと思います!!)

また、KMMという略称は非推奨であり、KMPという略称が推奨されています。

we are deprecating the “Kotlin Multiplatform Mobile” (KMM) product name. From now on, “Kotlin Multiplatform” (KMP) is the preferred term when referring to the Kotlin technology for sharing code across different platforms, regardless of the combination of targets being discussed.

https://blog.jetbrains.com/kotlin/2023/07/update-on-the-name-of-kotlin-multiplatform/

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

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


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