Open18

Kotlin Multiplatform Mobile入門

kenkenkenken

Projectを作成してSync Project with Gradle Filesしたら早速エラー😅

A problem occurred configuring root project 'KmmSandbox'.
> Could not resolve all files for configuration ':classpath'.
   > Could not resolve com.android.tools.build:gradle:8.0.2.
     Required by:
         project : > com.android.application:com.android.application.gradle.plugin:8.0.2
         project : > com.android.library:com.android.library.gradle.plugin:8.0.2
      > No matching variant of com.android.tools.build:gradle:8.0.2 was found. The consumer was configured to find a library for use during runtime, compatible with Java 8, packaged as a jar, and its dependencies declared externally, as well as attribute 'org.gradle.plugin.api-version' with value '8.0' but:
          - Variant 'apiElements' capability com.android.tools.build:gradle:8.0.2 declares a library, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares a component for use during compile-time, compatible with Java 11 and the consumer needed a component for use during runtime, compatible with Java 8
              - Other compatible attribute:
                  - Doesn't say anything about org.gradle.plugin.api-version (required '8.0')
          - Variant 'javadocElements' capability com.android.tools.build:gradle:8.0.2 declares a component for use during runtime, and its dependencies declared externally:
              - Incompatible because this component declares documentation and the consumer needed a library
              - Other compatible attributes:
                  - Doesn't say anything about its target Java version (required compatibility with Java 8)
                  - Doesn't say anything about its elements (required them packaged as a jar)
                  - Doesn't say anything about org.gradle.plugin.api-version (required '8.0')
          - Variant 'runtimeElements' capability com.android.tools.build:gradle:8.0.2 declares a library for use during runtime, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares a component, compatible with Java 11 and the consumer needed a component, compatible with Java 8
              - Other compatible attribute:
                  - Doesn't say anything about org.gradle.plugin.api-version (required '8.0')
          - Variant 'sourcesElements' capability com.android.tools.build:gradle:8.0.2 declares a component for use during runtime, and its dependencies declared externally:
              - Incompatible because this component declares documentation and the consumer needed a library
              - Other compatible attributes:
                  - Doesn't say anything about its target Java version (required compatibility with Java 8)
                  - Doesn't say anything about its elements (required them packaged as a jar)
                  - Doesn't say anything about org.gradle.plugin.api-version (required '8.0')
kenkenkenken

The consumer was configured to find a library for use during runtime, compatible with Java 8, packaged as a jar

とあるので、Android Studio > Settings... > Build, Execution, Deployment > Gradleの中のGradle JDKを確認したところ、確かにJava 8が指定されていた。

Incompatible because this component declares a component for use during compile-time, compatible with Java 11 and the consumer needed a component for use during runtime, compatible with Java 8

ともあるので、一旦これをJava 11に上げることでSync Project with Gradle Filesは成功。

kenkenkenken

次にRun 'androidApp'を実行したところ、次のエラー。

Build file '/Users/XXXX/AndroidStudioProjects/KmmSandbox/androidApp/build.gradle.kts' line: 1

An exception occurred applying plugin request [id: 'com.android.application']
> Failed to apply plugin 'com.android.internal.application'.
   > Android Gradle plugin requires Java 17 to run. You are currently using Java 11.
...

Gradle JDKをJava 17に上げることでRun 'androidApp'は成功。

スタートラインに立つまでが一番大変😇

kenkenkenken

Android/iOSともに初期画面は表示できた。

Android iOS
kenkenkenken

Android StudioからiOSのシミュレータを起動できるのは地味に便利だな〜
ちなみに、起動するシミュレータのバージョンはRun/Debug ConfigurationsExecution Tergetから変更できる模様。

kenkenkenken

公式サイトのExamine the project structureを見ると、各モジュールの説明が書いてある。

  1. shared module
    • shared is a Kotlin module that contains the logic common for both Android and iOS applications – the code you share between platforms.
    • It uses Gradle as the build system that helps you automate your build process. The shared module builds into an Android library and an iOS framework.
  2. androidApp module
    • androidApp is a Kotlin module that builds into an Android application.
    • It uses Gradle as the build system.
    • The androidApp module depends on and uses the shared module as a regular Android library.
  3. iosApp module
    • iosApp is an Xcode project that builds into an iOS application.
    • It depends on and uses the shared module as an iOS framework.
    • The shared module can be used as a regular framework or as a CocoaPods dependency, based on what you've chosen in the previous step in iOS framework distribution.
      • In this tutorial, it's a regular framework dependency.
kenkenkenken

iosAppモジュールがsharedモジュールを参照するためにデフォルトで行っている設定があまりよく分かっていないが、この部分sharedという名前のiOS frameworkを作成している模様。

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

iosApp.xcodeprojをXcodeで開き、TARGETS > Build Settingsの中のLinkingを確認したところ、Other Linker Flagsに-framework sharedとあるので、どうやらこの部分の設定によってiosAppモジュールがsharedモジュール(実体はiOS framework)を参照できるようになっている模様。

kenkenkenken

サポートされているTargetは公式サイトのKotlin/Native target supportに記載されている。

  • iosX64()
    • Apple iOS simulator on x86-64 platforms
  • iosArm64()
    • Apple iOS and iPadOS on ARM64 platforms
  • iosSimulatorArm64()
    • Apple iOS simulator on Apple Silicon platforms
kenkenkenken

公式サイトのConnect to platform-specific APIsにある通り、プラットフォーム固有のAPIに依存したロジックを共有したいときは、expected and actual declarationsというメカニズムが使用できる。

  1. sharedモジュール内で、共有したい関数やクラスをexpectキーワードを用いて宣言 (expected declaration)
  2. androidAppモジュールおよびiosAppモジュール内で、1.で作成した関数やクラスの実装をactualキーワードを用いて宣言 (actual declaration)

例:

// In `shared` module
expect fun randomUUID(): String

// In `androidApp` module
actual fun randomUUID(): String = UUID.randomUUID().toString()

// In `iosApp` module
actual fun randomUUID(): String = NSUUID().UUIDString()
kenkenkenken

Androidアプリ開発でお世話になっているモックライブラリのMockKはMultiplatform環境では使えないらしい。
数年前からGitHub Issueは立っていて、しばしば話題にはなっているみたいだけど、Multiplatform環境に対応する気配は今のところなさそう。
https://github.com/mockk/mockk/issues/322

代わりにMocKMPMockativeというライブラリを見つけた。そのうちお世話になるかもしれない。

kenkenkenken

公式サイトのAdd dependencies to your projectにあるdaysUntilNewYear()を追加した(commit)。

モックライブラリの話もあり、このままだとテストしづらいので、あとで少しリファクタリングする。
また、以下の観点でユニットテストを書く予定。

  • 年末
  • 年始
  • 閏年
kenkenkenken

公式サイトのAdd more dependenciesの通りに依存関係を追加したところ、以下のエラーが発生。

Cannot add a KotlinSourceSet with name 'iosMain' as a KotlinSourceSet with that name already exists.

すでに存在するiosMainを作成しようとしてエラーになっていたので、以下のようにby gettingを使うように修正した(commit)。

-   val iosMain by creating {
+   val iosMain by getting {
        dependencies {
            implementation("io.ktor:ktor-client-darwin:$ktorVersion")
        }
    }
kenkenkenken

これらのSourceSetは、公式サイトのNew approach to source set hierarchyにある機能をtargetHierarchy.default()の呼び出しにより有効化することによって追加されている模様。

なお、この部分の設定により、今回のプロジェクトではiosX64がTargetとしてさらに追加されている。

kenkenkenken

実装を進めていったところ、以下のコンパイルエラーが発生した。

Compilation failed: Internal compiler error: no implementation found for FUN DEFAULT_PROPERTY_ACCESSOR name:<get-parent> visibility:public modality:ABSTRACT <> ($this:kotlinx.coroutines.Job) returnType:kotlinx.coroutines.Job?
when building itable for CLASS INTERFACE name:Job modality:ABSTRACT visibility:public superTypes:[kotlin.coroutines.CoroutineContext.Element]
implementation in CLASS CLASS name:ChannelJob modality:FINAL visibility:private superTypes:[io.ktor.utils.io.ReaderJob; io.ktor.utils.io.WriterJob; kotlinx.coroutines.Job]
at /opt/buildAgent/work/8d547b974a7be21f/ktor-io/common/src/io/ktor/utils/io/Coroutines.kt (156:1)
CLASS CLASS name:ChannelJob modality:FINAL visibility:private superTypes:[io.ktor.utils.io.ReaderJob; io.ktor.utils.io.WriterJob; kotlinx.coroutines.Job]

 * Source files: 
 * Compiler version info: Konan: 1.8.21 / Kotlin: 1.8.21
 * Output kind: FRAMEWORK

org.jetbrains.kotlinx:kotlinx-coroutines-coreのバージョンを、公式サイト記載の1.6.4でなくAndroid Studioで推奨された最新版の1.7.1にしたことが関連しているようなので、原因を調べる。

kenkenkenken

Xcodeからビルドしようとすると、以下のエラーが発生する。

Command PhaseScriptExecution failed with a nonzero exit code

詳細を見ると、

Build file '/Users/XXXX/AndroidStudioProjects/KmmSandbox/androidApp/build.gradle.kts' line: 1

An exception occurred applying plugin request [id: 'com.android.application']
> Failed to apply plugin 'com.android.internal.application'.
   > Android Gradle plugin requires Java 17 to run. You are currently using Java 11.
...

と出力されており、これはTARGETS > Build Phrasesの中のRun Buildに記載されている

cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode

の実行時に発生している模様。

cd "$SRCROOT/.."
java -version
./gradlew :shared:embedAndSignAppleFrameworkForXcode

のように変更してJavaのバージョンを確認したところ、確かにopenjdk version "11.0.19" 2023-04-18 LTSが先頭に出力される…🤔