怠慢プログラマーの備忘録

怠慢でナマケモノなプログラマーの備忘録です。

Kotlin Multiplatform Mobile入門(備忘録)

モチベーション

  • AndroidiOSもネイティブで作れるけど疲れた
  • Kotlin大好き
  • Android大好き
  • AndroidStudioはXcodeに比べると神
  • iOSは好きだけどSwiftはKotlinに比べるとそんな好きじゃない
  • iOS SDKをKotlinで触りたいと2017年からずっと思ってた
  • 個人アプリがiOS先行、Androidほぼ未着手だからいい実験台がある
  • 自分のスキルスタック的にDartは除外(なんか肌に合わなかった)、ReactNativeもそこそこいけるけど現状アサインしている会社・プロジェクト周辺で特段Webフロントリソースは潤沢なのにモバイルエンジニアが...みたいなシーンがない
  • 基本的にドメインロジック以降が共通化できればよくて、UIはSwiftUI/Jetpack Composeを先々使っていきたいマン
  • UIはそれぞれのOSで異なった方が理想だと考える宗教
  • 最終的にはフルスクラッチで導入より、既存各ネイティブ構成に導入できるところまで
  • やるぞ!!!

参考

全てはここに書いてある

kotlinlang.org

実行環境

Android Studio Kotlin Multiplatform Mobile PlugInのインストール

Preferences -> Plugins -> Kotlin Multiplatform Mobile 検索でインストール f:id:ka0in:20210729025607p:plain

Restart IDEで再起動

Kotlinは当然入っていることとします。

Create KMM Project

KMM Applicationを選択 f:id:ka0in:20210729030128p:plain

通常のAndroidプロジェクト同様にPackageNameなどを入力。 f:id:ka0in:20210729030435p:plain

プロジェクト作成の途中でそれぞれのApplicationNameを入力します。 ディレクトリ構造がそのまま反映されるので基本的にデフォルトのままでFinishを選択。 ちなみにiOSのモジュール管理はデフォルトはCocoaPodsになってた、SPM使うときどうしようまぁいっか。

f:id:ka0in:20210729030649p:plain

構成

プロジェクトの作成が完了すると以下の構成になっています。 f:id:ka0in:20210729032120p:plain

androidApp

通常のAndroidプロジェクト同様MainActivityとレイアウトファイルなどが作成されています。 MainActivity内で共通化された後述のGreeting().greeting()をTextViewで出すだけの状態です。 MainActivity.kt

package app.freakshow.kmm_sample.android

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import app.freakshow.kmm_sample.Greeting
import android.widget.TextView

fun greet(): String {
    return Greeting().greeting()
}

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val tv: TextView = findViewById(R.id.text_view)
        tv.text = greet()
    }
}

iosApp

iOSの方はSwiftUIでプロジェクトが作成されます。 またCocoaPodsのpodfileと$pod install後のxcworkspaceも作成されています。

import SwiftUI
import shared

struct ContentView: View {
    let greet = Greeting().greeting()

    var body: some View {
        Text(greet)
    }
}

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

podfile

target 'iosApp' do
  use_frameworks!
  platform :ios, '14.1'
  pod 'shared', :path => '../shared'
end

podfile経由でsharedを連携させ、import shaerdにしているのでframework扱いとして共通部分を扱っているようです。

shared

iOS/Android双方で読んでいるGreeting内部ではPlatform().platformの文字列にHelloをつけて返しているだけのようです。 src/commonMain/kotlin/kmm-sample/Greeting.kt

class Greeting {
    fun greeting(): String {
        return "Hello, ${Platform().platform}!"
    }
}

ではPlatform内ではexpect classとして定義されています。 src/commonMain/kotlin/kmm-sample/Platform.kt

expect class Platform() {
    val platform: String
}

このactual(実装部分)がshared下のandroidMain/iosMainで実装される仕組みになっています。

src/androidMain/kotlin/kmm-sample/Platform.kt

actual class Platform actual constructor() {
    actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

src/iosMain/kotlin/kmm-sample/Platform.kt

import platform.UIKit.UIDevice

actual class Platform actual constructor() {
    actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

さらっとUIKit触ってますね。。。

build/実行

f:id:ka0in:20210729041159p:plain ※iosAppをAndroidStudioで実行時にパスの関係でcan't grab Xcode schemes with /usr/bin/xcodebuild -workspaceが出る場合はiosApp.xcworkspaceを一旦Xcodeで開いてproject/target両方で

//:configuration = Debug
EXCLUDED_ARCHS = 
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64

//:configuration = Release
EXCLUDED_ARCHS = 
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64

に設定すると解決するかと思います。 f:id:ka0in:20210729041221p:plain

iosApp/android/AppをそれぞれAndroidStudioから実行するとそれぞれのエミュレータが立ち上がり下記のようにOSとOSバージョンが表示されます。

f:id:ka0in:20210729042429p:plain f:id:ka0in:20210729042442p:plain