モチベーション
- AndroidもiOSもネイティブで作れるけど疲れた
- Kotlin大好き
- Android大好き
- AndroidStudioはXcodeに比べると神
- iOSは好きだけどSwiftはKotlinに比べるとそんな好きじゃない
- iOS SDKをKotlinで触りたいと2017年からずっと思ってた
- 個人アプリがiOS先行、Androidほぼ未着手だからいい実験台がある
- 自分のスキルスタック的にDartは除外(なんか肌に合わなかった)、ReactNativeもそこそこいけるけど現状アサインしている会社・プロジェクト周辺で特段Webフロントリソースは潤沢なのにモバイルエンジニアが...みたいなシーンがない
- 基本的にドメインロジック以降が共通化できればよくて、UIはSwiftUI/Jetpack Composeを先々使っていきたいマン
- UIはそれぞれのOSで異なった方が理想だと考える宗教
- 最終的にはフルスクラッチで導入より、既存各ネイティブ構成に導入できるところまで
- やるぞ!!!
参考
全てはここに書いてある
実行環境
- Android Studio 4.2.2
- Android Studio Kotlin PlugIn 1.5.21
- Android Studio Kotlin Multiplatform Mobile PlugIn 0.2.6
Android Studio Kotlin Multiplatform Mobile PlugInのインストール
Preferences -> Plugins -> Kotlin Multiplatform Mobile 検索でインストール
Restart IDEで再起動
Kotlinは当然入っていることとします。
Create KMM Project
KMM Applicationを選択
通常のAndroidプロジェクト同様にPackageNameなどを入力。
プロジェクト作成の途中でそれぞれのApplicationNameを入力します。 ディレクトリ構造がそのまま反映されるので基本的にデフォルトのままでFinishを選択。 ちなみにiOSのモジュール管理はデフォルトはCocoaPodsになってた、SPM使うときどうしようまぁいっか。
構成
プロジェクトの作成が完了すると以下の構成になっています。
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/実行
※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
に設定すると解決するかと思います。
iosApp/android/AppをそれぞれAndroidStudioから実行するとそれぞれのエミュレータが立ち上がり下記のようにOSとOSバージョンが表示されます。