batchのブログ

知見の備忘録

KMMでサンプル作ってみた

f:id:batch08:20210111213837p:plain

はじめに

KMMはKotlin Multiplatform Mobileといって、KotlinでiOSAndroidの共通なビジネスロジックを書いて時間と労力を節約しようというものです。(公式より)

最近は、Flutterのコミュニティがかなり活発で人気があるように感じますが、KMMはいい意味でも悪い意味でもiOSAndroidのUI部分を別々に記述できます。Flutterでは自分で工夫しないとiOSでもマテリアルデザインっぽいUIになってしまってiOSユーザからは少し見慣れないUIになりがちかなと思います。私もFlutterを触っていくつか趣味程度に個人アプリを開発してきましたが、Flutter開発に使うDartに比べてKotlinのほうが個人的に書きやすくて機能が豊富だったりサポートが手厚い感じがしていいなと思いました 。最近色々な豊富な機能が追加されてきているCoroutinesを使えるのもかなり大きいと思います。

KMMではそんなKotlinを使って今まで冗長になっていたiOSAndroidビジネスロジックのコードを共通化することで開発に必要なリソースの削減が可能になります。

作ったものはこちらにあります。

github.com

この記事では自分がKMMを触って得た知見をざっくりまとめていこうと思います。

この記事を読んでKMM触ったことない人がざっくりKMMでアプリを作るときどんな感じなのかイメージを掴んでもらえたらいいなと思います。

内容

モジュール・パッケージ構成

Android StudioでKMMのpluginがあってそれを入れるとNew ProjectするときにKMMの雛形ができたProjectを作ることができます。今回自分もそれを使いました。

そうすると、大きく androidAppiosAppsharedの3つのモジュールが作られます。

このsharedモジュールの中にiOSAndroidの共通コードを記述していくのですが、この中は大きくandroidMaincommonMainiosMainという3つのpackageに分けられています。

主にcommonMainの中に共通のロジックを書いていきます。自分がつくったサンプルではcommonMainの中のdataパッケージの中にDataSourceやそれにアクセスするRepositoryなどのロジックを入れました。これらのコードをAndroidはもちろん、iOSでも参照して使うことでプラットフォームごとに別々に同じようなコードを書くことを防いで開発リソースを削減するイメージです。

他のandroidMainiosMainは共通コードの中でもプラットフォームに依存していて別々に記述しないといけないものをKotlinで記述していきます。その際、まずcommonMainにそれぞれのプラットホームで実装する必要があるものをInterfaceのようにexpect classとして記述しておきます。そのあとにandroidMainiosMainに同じクラス名でactual classとして実装します。KMMでNew Projectした段階ではPlatform.ktがそれにあたって、デバイスの名前やOSの名前を取得しています。

ここで気をつけないといけないのが、それぞれのプラットフォームごとの実装クラスを書くときのクラスファイルを配置するpackage階層を`commonMain`と合わせないといけないことです。

たとえば、commonMain/hogeの直下にPlatform.ktexpect classとして作った場合、androidMainiosMainにおいてもandridMain/hoge/Platform.ktiosMain/hoge/Platform.ktとなるようにactual classのファイルを配置しないとエラーが起こり続けます。

これを使う場面としては、後述するローカルDBに接続するために必要なDriverの作成を各プラットフォームごとに実装するときなどに使います。具体的にはDatabaseDriverFactory.ktに当たります。

ちなみにこのcommonMainモジュールのbuild.gradle.ktsを見てみると

import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import dependencies.Dep

plugins {
    kotlin("multiplatform")
    kotlin("plugin.serialization")
    id("com.android.library")
    id("com.squareup.sqldelight")
}

kotlin {
    android()
    ios {
        binaries {
            framework {
                baseName = "shared"
            }
        }
    }
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(Dep.Kotlin.coroutines)
                implementation(Dep.Kotlin.serialization)
                implementation(Dep.Ktor.client)
                implementation(Dep.Ktor.serialization)
                implementation(Dep.SqlDelight.runtime)
                implementation(Dep.SqlDelight.coroutinesExtensions)
                implementation(Dep.Kodein.kodeinDi)
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
            }
        }
        val androidMain by getting {
            dependencies {
                implementation(Dep.AndroidX.design)
                implementation(Dep.Ktor.clientAndroid)
                implementation(Dep.SqlDelight.androidDriver)
            }
        }
        val iosMain by getting {
            dependencies {
                implementation(Dep.Ktor.clientIos)
                implementation(Dep.SqlDelight.nativeDriver)
            }
        }

こんな感じになっていて、commonMainandroidMainiosMainごとに依存関係を書く感じになってます。

どこまで共通化させられるのか

私のサンプルではMVVMな感じで作ったのですが、ViewとViewMode以外を共通化させてます。頑張ったらViewModelまで共通化させられるのか私のレベルではわかりませんが、ViewModelで参照しているUseCaseまで共通化させています。ViewModelでは宣言したメンバー変数たちをViewでObserveする必要があってそれぞれのプラットフォームでObserveできるオブジェクトにする必要があるため、それぞれのプラットフォームごとにViewModel以降作ったほうがいいかなと思います。ライフサイクルの兼ね合いもありますね。

iOSでもKotlin Coroutines Flowが使える

Flowはマルチプラットフォームに対応するようにも作られているようで、マルチプラットフォーム用のCoroutinesのバージョンはKotlin1.4.20現在で1.4.1-native-mtが最新のようです。 Concurrency and coroutines—Kotlin Multiplatform Mobile Docs

そのままでもiOSでFlowで流れてきた値を受け取れるようですが、使いにくいようなので私のつくったサンプルではCFlowというFlowのラッパーを作って使いました。 これはM3 Tech BlogのKotlin Multiplatform Mobileを使ってBrainf*ckエディタアプリを作るで紹介されていて自分も真似しました。そちらのブログでも紹介されているのですが、JetBrainsが開発したkotlinconf-appのFlowUtils.ktでも使われているやり方みたいです。 Androidでは普通にCoroutineScopeの中でcollectを使って受け取ってiOSでは下のように受け取ることができます。watchは先程のFlowのラッパーで実装したメソッドです。completionHandlerでエラーハンドリングもできるようです。

        searchActressUseCase.searchActress(searchedKeyWord: searchedKeyWord, completionHandler: { response, error in
            response?.watch { [weak self] response in
                if let actresses = response?.result.actress {
                    self?.actresses = actresses
                }
                
            }
        })

DataSource - Remote

サーバのAPIを叩くとき、素のAndroidアプリを開発するときはRetrofitとかを脳死で使うと思いますが、KMMではKtorを使うのが今の所のデファクトスタンダードみたいです。

ktor.io

こんな感じでクライアントを作って使います。

    private val dmmApiClient = HttpClient {
        install(JsonFeature) {
            val json = Json {
                ignoreUnknownKeys = true
                isLenient = true
            }
            serializer = KotlinxSerializer(json)
        }
    }

実際にGETする処理はこんな感じです。

dmmApiClient.get {
                    url(Constants.DMM_ENDPOINT)
                    parameter("api_id", Constants.API_ID)
                    parameter("affiliate_id", Constants.AFFILIATE_ID)
                    parameter("keyword", searchedKeyWord)
                }

このコードでは、https://api.dmm.com/affiliate/v3/ActressSearch?api_id=API_ID&affiliate_id=AFFILIATE_ID&keyword=searchedKeyWordというようなエンドポイントを叩いています。 あまり調べてもクエリパラメータを混ぜる方法がわからなかったので残しておこうと思います。 ちなみにそれっぽいこういう公式のドキュメントが出てきましたが僕には解読できませんでした。

ktor.io

DataSource - Local

Androidアプリ開発でいうとRoomに当たる部分です。サンプルアプリでは現在のデファクトスタンダードそうなSQLDelightというライブラリを使っています。 cashapp.github.io

ここで、DBにアクセスするためのSqlDriverを生成するコードが各プラットフォームに依存しているため、commonMainexpect classを書いてandroidMainiosMainactual classとして実装します。

これはあまり難しいことはないです。公式のドキュメントも充実してます。

kotlinlang.org

Flowにも対応しています。

cashapp.github.io

DI

KMMのDIではマルチプラットフォーム対応しているKodein-DIというライブラリがいいみたいです。 kodein.org

DIというとAndroidアプリエンジニアだとDaggerとかを想像して超難しいイメージがあるかもしれません。自分がそうです。しかしこのライブラリのDIはそれにくらべてとても簡単でわかりやすかったです。私自身あまり使ったことがないのですがKoinを使ったDIみたいな雰囲気を感じました。 これについても M3 Tech Blogの方でわかりやすく解説されています。

おわり

iOSアプリ開発者目線でKMMがどのような受け取られ方をしているかあまりわからないのですが、KMMを使った開発がプロダクト開発でも取り入れられて開発リソースの削減から余ったリソースで今までできなかったことに手を出せるような世界になったらいいなと思ってます。

KMMが今後開発者界隈でどのように受け取られてどうなっていくか楽しみです。