こんにちは! dely開発部でiOSエンジニアをしている@yochidrosです。
この記事は「dely #2 Advent Calendar 2020」の11日目の記事です。
昨日は@_kobuuukataさんの開発者向けのオンラインイベントを開催してわかった7つのポイントでした
普段,業務上はiOSアプリをゴリゴリ開発していますが、過去にAndroid・iOS両方を開発してたこともあり
一度に両OSを開発できたら良いと思っていました。
Flutter
やReact Native
などマルチプラットフォーム開発できるツールがありますがそれぞれで
メリット・デメリットがありなかなか導入に至るまでにはいかないケースがあると思います。
そこで今回は2020年の9月についにalpha版になったKMMについて調べてみました。
KMMとは?
Kotlin Multiplatform Mobileの略でiOS・Androidで共通のビジネスロジックを一つのコードで実現できるSDKです。 Kotlinで記述したものが各プラットフォームにネイティブなバイナリコードに変換されるので他のライブラリとも併用できます。
詳しくは公式サイトにも紹介があります。
KMPとKMMの違いは?
Kotlin Multi PlatformとKotlin Multiplatform Mobileの違いは簡単に言えばモバイルに特化しているという点です。 KMPはかくデスクトップのOSにコンパイルされるのに対してKMMはモバイルのOSにコンパイルすることができます。 KMPでもiOS/Androidに変換することができますがモバイルだけならKMMを利用すれば良いということになります。
使ってみよう
説明はさておき、実際に触ってみましょう。 実際に動かしたコードはこちらにあります。
※以下、こちらの環境で動作を確認してます
- Android Studio 4.2 Preview
- Kotlin 1.4.20-release-Studio4.2-1
- Xcode 12.2
新規プロジェクトを作成する前にプラグインを導入します。
Android StudioでPreference -> Plugins
でkotlin multiplatform
と調べるとKMMのプラグインがでてきますのでインストールします。
インストールしたらAndroid Studioを再起動しましょう。
再起動が完了したら新規プロジェクトのテンプレートにKMM Application
が増えているので選択します。
プロジェクトの名前を決めます。
今回はサンプルなので共通コードはShared
としています。
FINISH
を押すと共通コードのmoduleと各OSのプロジェクトが作成されます。
早速各OSで動かしてみましょう。
ビルドが通ってHello, #{OSのバージョン}
が表示されました!
※注意
エミュレーター・シュミレーターでの動作は確認できましたがiOSの実機での動作確認では注意が必要です。
iOSの場合はDeviceの選択がAndroidと同様に設定ができないです。
Configuration
でEdit Configuration
を選択します。
iOSのconfigを選択してExecution Target
を確認したい実機を選択します。
これで動くかと思いきやiOSだと開発者の証明書がないと実機に入れることができません。 証明書を設定せず動かそうとするとエラーが起きます。
error: Signing for "KmmiOSApp" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'KmmiOSApp' from project 'KmmiOSApp')
証明書の設定はXcodeでiOSのプロジェクトを開いて設定します。(サンプルではKmmiOSApp.xcodeproj
)
証明書の設定には Apple Developerアカウントが必要なのでない人は作っておきましょう。
これで再度Android Studioでビルド&Runすると実機のiOSで動作が確認できます。
もし以下のようなエラーが起きた時はClean Project
をしてからビルドしてみましょう。
error: Building for iOS, but the linked and embedded framework 'Shared.framework' was built for iOS Simulator. (in target 'KmmiOSApp' from project 'KmmiOSApp')
通信処理を共通化してみる
両OSとも動作確認ができたので次に共通の処理を書いていきます。 共通化できる部分は - 通信処理 - ログ送信 - etc.. が挙げられると思います。 その中で通信部分のところを共通化してみましょう。
今回は例としてGithubのREST APIを使って 自分のレポジトリを取得するAPIクライアントを書いてみます。
まず、通信やJSONパーサーのライブラリを導入します。
Shared/build.gradle.kts
val commonMain by getting { dependencies { implementation("io.ktor:ktor-client-core:1.4.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1") implementation("io.ktor:ktor-client-json:1.4.1") implementation("io.ktor:ktor-client-serialization:1.4.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1-native-mt") { version { strictly("1.4.1-native-mt") } } } } ... val androidMain by getting { dependencies { implementation("io.ktor:ktor-client-android:1.4.1") implementation("com.google.android.material:material:1.2.1") } } ... val iosMain by getting { dependencies { implementation("io.ktor:ktor-client-ios:1.4.1") } }
導入が完了したらコードを書いていきます。 最初に共通化したい処理を宣言します。
Shared/../commonMain/../GithubAPIClient.kt
expect class GithubAPIClient() { val client: HttpClient val dispatcher: CoroutineDispatcher }
あくまでこれは抽象的に宣言しているので実際に各プラットフォームに適したコードを定義します。
Shared/../androidMain/../GithubAPIClient.kt
actual class GithubAPIClient actual constructor() { actual val client: HttpClient = HttpClient(Android) { install(JsonFeature) { serializer = KotlinxSerializer(json = kotlinx.serialization.json.Json { isLenient = false ignoreUnknownKeys = true allowSpecialFloatingPointValues = true useArrayPolymorphism = false }) } } actual val dispatcher: CoroutineDispatcher = Dispatchers.Default }
Shared/../iosMain/../GithubAPIClient.kt
actual class GithubAPIClient actual constructor() { actual val client: HttpClient = HttpClient(Ios) { install(JsonFeature) { serializer = KotlinxSerializer(json = kotlinx.serialization.json.Json { isLenient = false ignoreUnknownKeys = true allowSpecialFloatingPointValues = true useArrayPolymorphism = false }) } } actual val dispatcher: CoroutineDispatcher = Dispacher(dispatch_get_main_queue()) }
iOSの場合はそのままではコルーチンが使えないので以下のようにdispatch_queue
を使って対応します。
class Dispacher(private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatchQueue) { block.run() } } }
あとは実際に通信する処理を書いていきます。 レスポンスはレポジトリ名とそのリンクだけを定義してます。
Shared/../commonMain/../GithubAPI.kt
@Serializable data class Repository( val name: String, @SerialName("html_url") val url: String, ) class GithubAPI { val apiClient: GithubAPIClient = GithubAPIClient() companion object { val BASEURL = "https://api.github.com/users/${自分のgithubのユーザーID}/repos" } fun fetchRepos(callback: (List<Repository>) -> Unit) { GlobalScope.apply { launch(apiClient.dispatcher) { val result = apiClient.client.get<List<Repository>>(BASEURL) callback(result) } } } }
これで共通部分の実装が完了しました。さくっとできましたね!
各OS毎でビルドをするとアプリから利用できるようになります。
- iOS
import UIKit import Shared class ViewController: UIViewController { @IBOutlet weak var textView: UITextView! override func viewDidLoad() { super.viewDidLoad() GithubAPI().fetchRepos { (repos) in DispatchQueue.main.async { self.textView.text = repos.map { $0.name }.joined(separator: "\n") } } } }
- Android
import com.yochidros.kmmsample.Shared.GithubAPI class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) GithubAPI().fetchRepos { runOnUiThread { val tv: TextView = findViewById(R.id.text_view) tv.text = it.map { it.name }.joinToString("\n") } } } }
動かしてみると実際に画面に自分のレポジトリの名前がでていますね!
最後に
いかがでしたでしょうか? KMMを使って共通のロジックとなりうる部分を一つのコードで処理することができたと思います。 iOSの場合はCocoaPodsにも対応しているので 共通ロジックとアプリケーションでレポジトリを分けて開発することもできると思います。
まだ、Alpha版なのでいきなり導入することはできないかと思いますが、少しでも共通できるものは共通化しておけば 相互の仕様の齟齬が少なくなると思います。
明日は @yyamanoi1222のCloud Runで手軽にサーバーレス・SSR(サーバーサイドレンダリング)です!お楽しみに!
また、delyではエンジニアを絶賛募集中です!ご興味があればこちらのリンクからお気軽にエントリーください!
さらに定期的にTechTalkというイベントも開催しているので、delyについて詳しく知りたい方は是非参加してみてください!