Zenn
🔰

【KMP入門】プロジェクトの作成からサンプルアプリの起動まで

2025/01/23に公開

Kotlin Multiplatform (KMP) とは

AndroidネイティブアプリとiOSネイティブアプリを同時に開発するためのフレームワークです[1]

本来はAndroidネイティブ用とiOSネイティブ用で別々のプロジェクトを作り、別々のコードを書き…とするところを、同じプロジェクトとしてまとめられ、一部のコードをKotlinで共通化できるようになります。

プロジェクトの作成

KMPで新規プロジェクトを作るには Kotlin Multiplatform Wizard を使用します。
https://kmp.jetbrains.com

Kotlin Multiplatform Wizard の選択画面

今回はiOSの「Do not share UI (use only SwiftUI)」を選択します。他の設定は初期状態のままで構いません。

Share UI (with Compose Multiplatform UI framework)」の方はCMPと呼ばれるもので、簡潔に言うとKotlin版Flutterなのですが、こちらはまた別の記事でご説明します。

Fleetのダウンロード

次に、KMP開発元の公式エディターであるFleetをダウンロードします。
https://www.jetbrains.com/ja-jp/fleet/

ダウンロードが完了したらFleetを起動し、「Open...」から先ほど作成したプロジェクトを開きます。

Fleetの初期起動画面

KMPプロジェクトの見方

Fleetでサンプルプロジェクトを開いた時の画面

ぐわーっとファイルとディレクトリーが並んでいて驚くと思いますが、重要なのは

  • composeAppAndroidアプリ用のコード(Kotlin) + 設定ファイル
  • iosAppiOSアプリ用のコード(Swift) + 設定ファイル
  • sharedAndroid/iOSで共有するコード(Kotlin) + 設定ファイル

の三つです。それ以外のファイルやディレクトリーは基本的に触らなくても開発できます。

とりあえずアプリを起動してみる

Kotlin Multiplatform Wizard で作成したプロジェクトには簡単なサンプルコードが含まれています。

まずはアプリを動かしてみた方が分かりやすいと思うので、シミュレーターで起動してみましょう。

実行は画面右上にあるRunボタンから出来ます。

Fleetの画面右上にあるRunボタン

Runボタンを押すと実行するデバイスの選択画面が出ます。
もし緑の雷マークが付いている場合はFleetが準備中なのでしばらく待ちましょう。

Runボタンを押した後に出るダイアログ

普段AndroidやiOSアプリを開発している方は最初から実行する機種名が出ていると思いますが、そうでない場合は「No devices available」という表示になるか、赤い🔴に!マークのアイコンが付きます。
また、iOSアプリとして実行できるのはmacのみです。WindowsやLinuxで開発している方はAndroidのみとなります。

実行に成功すると以下のようなアプリが立ち上がります。

KMPのサンプルアプリの動作画面(Android、iOS)

Click me!」と書かれたボタンをタップするとAndroid、iOSそれぞれに合わせたアイコンが表示されます。

コードを読んでみる

shared

sharedディレクトリーの中身

sharedは前述した通り、Android、iOSで共有するコードが格納されています。
普段iOSアプリを開発している方はcommonMain/kotlin/org/example/projectなどの深い階層に驚くと思いますが、Androidアプリ開発ではお馴染みの仕様です。

まずはcommonMain/kotlin/org/example/project/Greeting.ktを読んでみます。

Greeting.kt
package org.example.project

class Greeting {
    private val platform = getPlatform()

    fun greet(): String {
        return "Hello, ${platform.name}!"
    }
}

Greetingというクラスが定義されていて、greet()メソッドではgetPlatform().nameで得られた文字列を返しているのが分かります。
getPlatform()の定義はPlatform.ktにあります。

Platform.kt
package org.example.project

interface Platform {
    val name: String
}

expect fun getPlatform(): Platform

ところが、getPlatform()はありますが実装がありません。

getPlatform()の実装はandroidMainPlatform.android.ktiosMainPlatform.ios.ktにそれぞれあります。

Platform.android.kt
package org.example.project

import android.os.Build

class AndroidPlatform : Platform {
    // OSのAPIレベルを取得
    override val name: String = "Android ${Build.VERSION.SDK_INT}"
}

actual fun getPlatform(): Platform = AndroidPlatform()
Platform.ios.kt
package org.example.project

import platform.UIKit.UIDevice

class IOSPlatform : Platform {
    // OSの種類とバージョンを取得
    override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

actual fun getPlatform(): Platform = IOSPlatform()

Androidはもちろん、iOSもKotlinでネイティブ機能が呼び出せます。ただし画面に表示させたり、音を鳴らしたりする処理は共有コードとして書くことが出来ず、それぞれのネイティブコード(androidAppiosApp内のコード)として書く必要があります。

このように、KMPの共有コードでは、一つのメソッドをそれぞれのプラットフォーム用に分けて実装することが出来ます。
もちろん、不要であれば全ての共有コードをcommonMain内だけで完結することも可能です。

ちなみにshared/build.gradle.ktsでは、共有コードで使用する外部ライブラリーを追加できます。

iosApp

iosAppディレクトリーの中身

普段iOSアプリを開発している人にはお馴染みのファイルが並んでいます。

ContentView.swift
import SwiftUI
import Shared

struct ContentView: View {
    // SwiftUIで変数の更新を画面に反映させる、変数の宣言時に@Stateなどを付ける
    @State private var showContent = false
    var body: some View {
        VStack {
            Button("Click me!") {
                withAnimation {
                    showContent = !showContent
                }
            }

            if showContent {
                VStack(spacing: 16) {
                    // Swiftのツバメのアイコンを表示
                    Image(systemName: "swift")
                        .font(.system(size: 200))
                        .foregroundColor(.accentColor)
                    // KMPの共通コードを呼び出して画面に表示させる
                    Text("SwiftUI: \(Greeting().greet())")
                }
                .transition(.move(edge: .top).combined(with: .opacity))
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

二行目のimport Sharedで、KMPの共有コードをインポートしています。

composeApp

composeAppディレクトリーの中身

src/androidMain以下に、普段Androidアプリを開発している人にはお馴染みのファイルが並んでいます。
src/commonMainには Jetpack Compose に依存しないファイルを入れるようです。
サンプルではcompose-multiplatform.xmlという名前で、アプリに表示させるComposeのアイコンデータ(↑の起動画面にある箱みたいなアイコン)が格納されています。

App.kt
package org.example.project

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview

import kotlinproject.composeapp.generated.resources.Res
import kotlinproject.composeapp.generated.resources.compose_multiplatform

// @Composableを付けると変数の更新を画面に反映できるようになる
@Composable
@Preview
fun App() {
    MaterialTheme {
        // showContentが変化すると画面が更新される
        var showContent by remember { mutableStateOf(false) }
        Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
            Button(onClick = { showContent = !showContent }) {
                Text("Click me!")
            }
            AnimatedVisibility(showContent) {
                // KMPの共通コードを呼び出して文字列を取得
                val greeting = remember { Greeting().greet() }
                Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
                    Image(painterResource(Res.drawable.compose_multiplatform), null)
                    Text("Compose: $greeting")
                }
            }
        }
    }
}

おわりに

最初にKMPでプロジェクトを作成すると複雑そうに見えますが、実際はAndroid、iOS、共有部分の三つに分かれているだけだとご理解いただけたら幸いです。

次回は簡単なカウンターアプリを作ってみます。
https://zenn.dev/zoome/articles/7264cb47c13ef7

参考文献

https://pleiades.io/help/fleet/getting-started.html

備考

本記事は以下の環境で検証しました

Fleet: 1.44.151
Android Studio: 2024.1 (AI-241.18034.62.2411.12071903)
Xcode 16.2
脚注
  1. 更にはデスクトップアプリ、Web、サーバーサイドKotlinも一つのプロジェクトでまとめられ、一部のコードをKotlinで共通化できます ↩︎

GitHubで編集を提案
合同会社zoome(ズーム)

Discussion

ログインするとコメントできます