Compose Multiplatform 実践:CMPのプロジェクト構造理解とコンパイル設定

はじめに

Compose Multiplatform (略称CMP)
昨日、CMPのシミュレータ設定を完了したばかりです

前から言い忘れていましたが
実は私はこのシリーズの記事で
初心者モバイルアプリ開発をしていない人でも
入門できるようにしたいと思っています
そのため、冗長に感じる部分や詳しく説明している箇所があります
ご了承くださいXDDD

今日はCMPプロジェクトの構造と
Gradle設定の調整
そしてCMPプロジェクトにおけるlib.version.tomlの用途について紹介します

CMPプロジェクト構造

これはCMPプロジェクトを作成したときの
デフォルトのプロジェクト構造です

YourProjectName
├── build
├── composeApp
│   ├── build
│   ├── src
│   │   ├── commonMain
│   │   ├── commonTest
│   │   ├── iosMain
│   │   └── desktopMain
│   └── build.gradle.kts
├── gradle
│   ├── wrapper
│   │   └── libs.versions.toml
├── iosApp
│   ├── iosApp
│   └── iosApp.xcodeproj
├── .gitignore
├── build.gradle.kts
├── gradle.properties
├── local.properties
└── settings.gradle.kts

ここで各フォルダやファイルの用途を整理しました
初心者の方がより早く理解できるよう願っています

  • YourProjectName: プロジェクト名であり、プロジェクト全体のrootフォルダです
  • build: コンパイル過程での出力ファイル、これはコンパイルによって生成されるため.gitignoreに追加できます
  • composeApp: Compose Multiplatformアプリケーションのソースコードとその設定を含みます
    • build: CMPのコンパイル出力、これはコンパイルによって生成されるため.gitignoreに追加できます
    • src: CMPのプログラムコードディレクトリ
      • commonMain: CMPプロジェクト共通のロジックコードディレクトリ
      • commonTest: CMPプロジェクトのテストコードディレクトリ
      • iosMain: iOSの実装コードディレクトリ
      • desktopMain: desktopの実装コードディレクトリ
    • build.gradle.kts: CMPのGradle設定ファイル
  • gradle: Gradle関連の設定ファイル
    • wrapper: Gradle Wrapper関連ファイルを含む
    • libs.versions.toml: プロジェクトで使用する依存関係のバージョンを定義
  • iosApp: iOSプロジェクトのrootフォルダ
    • iosApp: iOSのプログラムコードディレクトリ
  • iosApp.xcodeproj: iOSのXcodeプロジェクトファイル
  • .gitignore: Gitバージョン管理で無視すべきファイルやディレクトリを定義
  • build.gradle.kts: ルートディレクトリのGradle設定ファイル
  • gradle.properties: Gradleプロパティファイル
  • local.properties: ローカル設定のプロパティファイルを定義
  • settings.gradle.kts: CMPプロジェクト設定を定義するGradleファイル
lib.version.tomlを使用したGradle依存関係の設定

libs.versions.tomlはプロジェクトの依存関係バージョンを管理するための設定ファイルです
特にGradleを使用してビルドするプロジェクトにおいて重要です

Gradle公式ドキュメントによると
この機能はGradle 7.0バージョンでリリースされたものです
また、彼らはこの機能をversion catalogsと呼んでいます

このファイルはTOML(Tom’s Obvious, Minimal Language)形式を使用して依存関係のバージョン情報を定義し
プロジェクト内でこれらの情報を一元管理することで、保守性と可読性を向上させます

以下はlibs.versions.tomlの例とその説明です:

[versions]
agp = "8.2.0"
kotlin = "2.0.10-RC"
androidx-activityCompose = "1.9.0"

[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }

主要部分の説明

  • バージョン定義 [versions]:
    この部分はプロジェクトで使用される各依存関係のバージョン番号を定義します
    例えば、kotlin = “2.0.10-RC”はKotlinのバージョンが2.0.10-RCであることを示します

  • 依存関係定義 [libraries]:
    この部分はプロジェクトで実際に使用される依存関係とそのバージョン情報を定義します
    各ライブラリはmoduleversion.refを定義します
    ここでmoduleは依存関係のMavenであり
    version.refは上で定義したバージョン番号を参照します
    • 例えば androidx-activity-compose = { module = “androidx.activity:activity-compose”, version.ref = “androidx-activityCompose” }
    • は androidx-activityCompose 標準ライブラリのバージョンが androidx-activityCompose で定義されたバージョン番号1.9.0を参照していることを示します
  • プラグイン定義 [plugins]:
    この部分はプロジェクトで使用されるプラグインとそのバージョン情報を定義します
    各プラグインはidversion.refを定義します
    ここでidはプラグインの識別子であり
    version.refも上で定義したバージョン番号を参照します
    • 例えば、kotlinMultiplatform = { id = “org.jetbrains.kotlin.multiplatform”, version.ref = “kotlin” }
    • はkotlinMultiplatform PluginのバージョンがKotlinで定義されたバージョン番号2.0.10-RCを参照していることを示します
  • 上記の.tomlを設定したら
    プロジェクトを同期した後
    Gradle設定で直接libs.xxx.xxの形式を使用して依存関係を設定できます
    例:

    implementation(libs.androidx.activity.compose)

  • この設定方法により、プロジェクトの依存関係バージョン管理がより一元化され、バージョンアップグレードと保守が容易になります。libs.versions.tomlファイルを通じて、プロジェクトは全ての依存関係のバージョン情報を明確に把握でき、複数の場所でバージョン番号を重複定義することによる混乱を避けることができます。

  • また、こちらは.tomlにmigrationしてversion catalogsを使用する際に発生する問題について書かれていますので、ご参考ください
build.gradle.kts(:composeApp)

build.gradle.kts(:composeApp)CMPプロジェクトを設定するためのGradleビルドスクリプトファイルです
これは`Kotlin DSL(Domain Specific Language)を使用してビルド設定を定義しています<br> この方法はより強力な型安全性(Null safety)とより良いIDE サポート`を提供します

主にアプリのコンパイル時の動作に影響します

しかし、CMPプロジェクトのbuild.gradle.ktsの内容は長いため
パートに分けて説明します

  • pluginsブロック:プラグインをインポートするために使用します
    対応するプラグインをインポートするには、lib.version.tomlで宣言されたプラグインを使用します
    以下のように:
    plugins {
      alias(libs.plugins.multiplatform)
      alias(libs.plugins.compose.compiler)
      alias(libs.plugins.compose)
      alias(libs.plugins.android.application)
      alias(libs.plugins.buildConfig)
      alias(libs.plugins.kotlinx.serialization)
    }
    

kotlinブロック:
ここでは主にCMPプロジェクトの設定項目を配置します
例えば、共有ファイル(String、画像など)の設定
コンパイルJDK
または異なるターゲットプラットフォームの設定など

ここで簡単に説明します:

  • androidTarget > compilerOptions > jvmTarget.set(JvmTarget.JVM_17) JDK 17を使用してコンパイルするよう設定

  • cocoapodsGradleを通じてcocoapodsをインポートし、iOSのフレームワークを使用

  • listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target -> ...
    iOS向けの設定、例えばcinteropsを使用してiOSと橋渡しする
    CMPが指定されたiOSフレームワークを使用できるようにする

  • sourceSets > androidMain.dependenciescommonMain.dependenciesなど:
    ここでは異なるプラットフォーム向けにインポートしたい依存関係を指定できます
    例えば:androidMainブロックはandroidで使用するライブラリをインポート
    commonMainブロックは共通ロジックで使用するライブラリをインポート
    さらにiosMainを使用してiOSで使用するライブラリもインポートできます

ただし、下の例では橋渡し方式を使用しているため
以下を使用しています
listOf(iosX64(), iosArm64(), iosSimulatorArm64())
iOSフレームワークをインポートするための方法ですが
ここでは簡単に触れるだけにします
後の章でiOSの橋渡しについてより詳細に説明します

  • 以下は簡単なbuild.gradle.ktsの例です
kotlin {

    androidTarget {
        @OptIn(ExperimentalKotlinGradlePluginApi::class)
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_17)
        }
    }

    cocoapods {
        summary = "Bidapp ads for kotlin multiplatform"
        version = "1.0"
        homepage = "https://github.com/JetBrains/kotlin"
        ios.deploymentTarget = "15.4"
        podfile = project.file("../iosApp/Podfile")
        name = "composeApp"
        pod("Google-Mobile-Ads-SDK")

        framework {
            baseName = "composeApp"
            linkerOpts.add("-lsqlite3")
            isStatic = true
            binaryOption("bundleId", "xxxx.edu")

        }

    }
  

    listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target ->
            
            ...

        val frameworkPath = baseDir.resolve(targetArchitecture)
    
        target.compilations.getByName("main") {

            cinterops {
                create("GoogleMobileAds") {
                    defFile(project.file("src/nativeInterop/cinterop/GoogleMobileAds.def"))
                    compilerOpts(
                        "-framework",
                        "GoogleMobileAds",
                        "-F$frameworkPath"
                    )

                }
            }
        }

        target.binaries.all {
            linkerOpts(
                "-framework",
                "GoogleMobileAds",
                "-F$frameworkPath"
            )
        }
    }

    sourceSets {


        androidMain.dependencies {
            implementation(compose.preview)
            implementation(libs.androidx.activity.compose)
            implementation(libs.google.ads)

        }
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material3)
            implementation(compose.ui)
            implementation(compose.components.resources)
            implementation(compose.components.uiToolingPreview)
            implementation(libs.koin.core)
            implementation(libs.koin.compose)
            implementation(libs.koin.compose.viewmodel)
            implementation(libs.navigation.compose)
            implementation(libs.datastore.core)
            implementation(libs.ktor.serialization.kotlinx.json)
            implementation(libs.androidx.room.runtime)
            implementation(libs.sqlite.bundled)
            implementation(libs.kotlinx.datetime)

        }

    }
}
  • 可以看到
    如果你想要在
    共用邏輯導入material3
    就直接在commomMain的block中導入即可
    不過可能會有人好奇
    為啥這邊是compose.material而不是libs.xxxxxx
    這是因為
    當你導入KMM插件後
    他有內建一下compose專案常用的library
    讓你可以直接用
    不用自己再去lib.version.toml中宣告
commonMain.dependencies {
    implementation(compose.material3)
}

You might also enjoy