Open9

Kotlin multiplatform mobileにAndroidアプリを移行

AniokraitAniokrait

プロジェクトレベルのbuild.gradleに定義していたライブラリの変数は、以下のように移行する。

//移行前
buildscript {
  ext {
    kotlin_version = '1.8.20'
  }
}

//変数宣言なしにどこからでも参照可能
dependencies {
  classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
//移行後
buildscript {
  extra.apply {
    set("kotlin_version", "1.8.20")
  }
}

//使用する箇所で変数を宣言する
val kotlin_version: String by project
dependencies {
  classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}

が、buildSrcやバージョンカタログを使ったほうが筋は良さそう。
https://qiita.com/mangano-ito/items/7e13f1988f9da61746b8

AniokraitAniokrait

順番が前後するが、Koinの公式セットアップサンプルがbuildSrcのやり方をしているので、それを踏襲しextを使うのはやめる。
https://insert-koin.io/docs/reference/koin-mp/kmp#gradle-dependencies
buildSrc方式でやるには以下の手順で行う。

  1. プロジェクト直下にbuildSrcディレクトリを作成。
  2. build.gradle.ktsをbuildSrcディレクトリ直下に作成し以下を記述。
plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
}
  1. buildSrc/src/main/java/Dependencies.ktを作成し以下を記述。
object Versions {
    const val koin = "3.2.0"
}

object Deps {

    object Koin {
        const val core = "io.insert-koin:koin-core:${Versions.koin}"
        const val test = "io.insert-koin:koin-test:${Versions.koin}"
        const val android = "io.insert-koin:koin-android:${Versions.koin}"
    }
}

上記はKoinの例だが、他のdependenciesでも同様にVersions配下にバージョンを書けば、グローバルに参照できるようになる。

AniokraitAniokrait

HiltはJavaクラスを生成するDIライブラリなので、commonMainで使用することはできない。
KoinはピュアKotlinのライブラリなので、commonMainでDIしたい場合はこちらを使う。
https://www.reddit.com/r/androiddev/comments/u33m5e/is_there_extra_benefits_of_using_koin_instead_of/

Syer10 1 yr. ago
You can't use Dagger with KMM since Dagger generates java classes, which cannot be used with Kotlin Multiplatform. The closest thing to Dagger for Kotlin Multiplatform is Kotlin-Inject, but people find Koin easier to use since it's closer to a Service Locator then a full DI.

https://insert-koin.io/docs/reference/koin-mp/kmp

Koin is a pure Kotlin library and can be used in your shared Kotlin project.

AniokraitAniokrait

Hiltが使えないことに気づくまで、kaptをbuild.gradle.ktsで使う方法を調べていたが無駄になってしまった。
普通に

dependencies {
    kapt("com.google.dagger:hilt-android-compiler:$hilt_version")
}

と書くと、sync時にエラーが起きる。

dependencies {
    implementation("com.google.dagger:hilt-compiler:2.37")
    // configure kapt to utilize
    configurations.getByName("kapt").dependencies.add(
        org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency(
            "com.google.dagger",
             "hilt-compiler",
              "2.37"
        )
   )
}

のようにしたらsync自体はできたが、結局commonMain配下のソースコード中でhiltのアノテーションをインポートできなかった。
stackoverflowの回答をよく読んだらandroidMainの中でhiltを使う方法だった。
https://stackoverflow.com/questions/59321848/using-kapt-with-multiplatform-subproject

AniokraitAniokrait

buildSrcの中でライブラリバージョンやライブラリ名を定義していればあとはsharedのbuild.gradle.ktsのdependenciesで呼び出すだけでKoinが使えるようになる。

sourceSets {
        val commonMain by getting {
            dependencies {
                with(Deps.Koin) {
                    api(core)
                    api(test)
                }
            }
        }
AniokraitAniokrait

cocoaPodsのありなしで、共通モジュールのbuild.gradle.ktsへのiOSの指定の仕方が異なる。

cocoaPodsあり

kotlin {
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    cocoapods {
        summary = "Some description for the Shared Module"
        homepage = "Link to the Shared Module homepage"
        version = "1.0"
        ios.deploymentTarget = "14.1"
        podfile = project.file("../iosApp/Podfile")
        framework {
            baseName = "shared"
        }
    }

cocoaPodsなし

kotlin {
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
        }
    }
AniokraitAniokrait

Koinのイニシャライズ

Android

androidApp配下のMainApplication.kt

class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            //モジュール内でContextを読み込みたい場合、以下が必要。
           //またandroidContext()を呼び出すためには、androidMainに※のdependencyが必要
            androidContext(this@MainApplication)
            //モジュールの読み込み
            modules(appModule() + androidModule)
        }
    }
}

sharedのbuild.gradle.kts

val androidMain by getting {
    dependsOn(commonMain)
    dependencies {
        with(Deps.Koin) {
            api(android)
        }
    }
}

androidはbuildSrcのDependencies.ktで定義している。

object Deps {
    object Ksp {
        const val ksp = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:${Versions.kspPlugin}"
    }

    object Koin {
        const val core = "io.insert-koin:koin-core:${Versions.koin}"
        const val test = "io.insert-koin:koin-test:${Versions.koin}"
        const val android = "io.insert-koin:koin-android:${Versions.koin}"
        const val annotations = "io.insert-koin:koin-annotations:${Versions.koinAnnotations}"
        const val kspCompiler = "io.insert-koin:koin-ksp-compiler:${Versions.kspPlugin}"
    }

}

iOS

iosMain内に定義したHelper.ktのinitKoin()を呼び出す。
shared/iosMain/kotlin/Helper.kt

fun initKoin(){
    startKoin {
        modules(appModule())
    }
}

iosApp/iosApp/iOSApp.swift

@main
struct iOSApp: App {
    init() {
        HelperKt.doInitKoin()
    }
    
	var body: some Scene {
		WindowGroup {
			ContentView()
		}
	}
}

AniokraitAniokrait

sync時に発生するエラーが解消できない

サンプルプロジェクトなどを参考に見様見真似で設定ファイルをいじってきたが、以下のようなエラーがsync時に発生してにっちもさっちもいかなくなった。

Caused by: java.lang.IllegalStateException: Artifacts of dependency org.jetbrains.kotlinx:kotlinx-coroutines-core-metadata:1.4.1 is built by old Kotlin Gradle Plugin and can't be consumed in this way

build.gradle.ktsやその他のファイル群も確認したが、サンプルとどこが違うのか特定できず。
検索しても情報がでてこずお手上げ。
結局KMMプロジェクトをいちから作成して、既存のソースコードを移植する方針に変更。