Kotlin のマルチプラットフォーム対応、アツいですね。
- KotlinConf 2017 Keynote レポート | TechBooster
- JetBrains/kotlinconf-app: KotlinConf Schedule Application
上の kotlinconf-app の対応プラットフォームは、
- サーバーサイド(Kotlin for Server-side, Ktor)
- Webページ(Kotlin/JS, React)
- Android(Kotlin/JVM)
- iOS(Kotlin/Native)
となっています、すご!
Xamarin と比べてどうよ?
普段 Xamarin を使用して Android/iOS アプリを開発しているので、クロスプラットフォームアプリ開発技術が増えて嬉しい限り。
しかも Kotlin で書けるのはとてもよいですね。
正直、C# よりも Kotlin の方が、書いていて気持ちよいですよね。
Kotlin のマルチプラットフォーム対応におけるトピックは2つ。
- Kotlin/Native による iOS ネイティブアプリのビルドが可能
- "Common Module" と呼ばれる手法で、プラットフォーム毎の処理を共通化できる
これらについて、 Xamarin と比較しながら見ていきましょうか。
1. Kotlin/Native による iOS ネイティブアプリのビルドが可能
これは LLVM 技術を利用して、Kotlin のソースコードから iOS のネイティブバイナリを出力する、というものです。LLVM を使用していることから、iOS 以外にも対応可能とされています。
Kotlin/Native(iOS)のソースコード からは、 AppDelegate
や UIResponder
などの CocoaTouch フレームワークの API が Kotlin から使えることが分かります。
さてこの Kotlin/Native、 Xamarin では Xamarin.iOS の AOT(Ahead of Time Compile)で実現していることに近いです。
(Xamarin.iOS の場合、C# のソースコードは中間言語に変換され、デバッグ時はそれが使用される、リリースビルドでは中間言語からネイティブバイナリに”事前”コンパイルされる、という手法なので少し異なります。)
また、Swift/Obj-C でない言語から CocoaTouch の API を直接呼ぶ、という意味では、
- RoboVM - Java から CocoaTouch の API が呼べた(後に Xamarin が買収して消滅)
- RubyMotion - Ruby から CocoaTouch の API が呼べる(最近話題にならないね)
などがあり、類似の仕組みを採用していると思われます。
このような「プラットフォーム固有のAPIを、インターフェースを(極力)変えずに他の言語(ここではKotlin)から呼べるようにする」ことを「薄いラッパーを提供する」と私が勝手に言っており、その薄いラッパーがあるが故に、
- プラットフォームのAPI知識がそのまま活かせる
- 同じ言語で開発できるしデバッグできる
というメリットを生みます。
2. "Common Module" と呼ばれる手法で、プラットフォーム毎の処理を共通化できる
1 は "iOS の API が Kotlin から呼べる" のみであって、これだけではコード共通化に寄与しません。
コード共通化の "手法" を提供するのがこの Common Module です。
これは、プラットフォーム毎に異なるAPIに対して、共通のAPIを定義し、それにプラットフォーム固有の実装を注入する、というものです。
kotlinconf-app のソースを例に説明してみると、日付を扱う Date
クラスを Common Module を使って作っています。
まず、共通のAPIとしての Date
クラスを common プロジェクト? に定義しています。実際のコード の転載がこちら。
package org.jetbrains.kotlinconf
expect class Date() {
fun getDate(): Int
fun getMonth(): Int
fun getFullYear(): Int
fun getHours(): Int
fun getMinutes(): Int
fun getTime(): Number
}
expect operator fun Date.compareTo(otherDate: Date): Int
expect fun parseDate(dateString: String): Date
expect fun Date.toReadableDateString(): String
expect fun Date.toReadableTimeString(): String
fun Date.toReadableDateTimeString() = "${toReadableDateString()} ${toReadableTimeString()}"
getDate
や parseDate
など、ありがちなメソッドが定義されていますが、ここに実装はなく、代わりに見慣れないキーワード expect
が付いています。
expect
は、「プラットフォーム毎に実装が期待されるモノ」を示すキーワードだと私は解釈しました。
では実装はどこにあるのか?と探してみると、Android (というかJVM)での実装が common-jvm/src/main/kotlin/JvmDate.kt
に見つかります。
package org.jetbrains.kotlinconf
import java.text.SimpleDateFormat
import java.util.*
import java.util.Calendar.*
actual class Date {
private val calendar: Calendar
actual constructor() {
calendar = Calendar.getInstance()
}
constructor(date: java.util.Date) {
calendar = Calendar.getInstance().apply {
time = date
}
}
val date: java.util.Date get() = calendar.time
actual fun getDate() = calendar[DAY_OF_MONTH]
actual fun getMonth() = calendar[MONTH]
actual fun getFullYear() = calendar[YEAR]
actual fun getHours() = calendar[HOUR_OF_DAY]
actual fun getMinutes() = calendar[MINUTE]
actual fun getTime(): Number = calendar.timeInMillis
override fun equals(other: Any?): Boolean = other is Date && other.calendar.time == calendar.time
}
<以下省略>
Android では Java SE API が使用できるので、 java.util.Date
や java.util.Calendar
を使用して実装をしています。 expect に対してこちらには actual
というキーワードが付いていますね。
さらに、 common-jvm/build.gradle
には、
apply plugin: 'kotlin-platform-jvm'
apply plugin: 'kotlinx-serialization'
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version"
testCompile "junit:junit:4.12"
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
expectedBy project(":common")
}
<以下省略>
expectedBy project(":common")
という記述があり、これが「common プロジェクトで期待された実際のコードである」ことを定義しています。これがビルドシステムによって解釈され、空っぽの common/Date
クラスが common-jvm/Date
クラスに置き換えられるのでしょう。
同じように iOS 側の Date の実装も…と思ったのですがみつからず(ない?)、さらに他の Date 実装として JavaScript(Kotlin/JS) 側での actual な Date が common-js/src/main/kotlin/JsDate.kt
にありました。
package org.jetbrains.kotlinconf
actual external class Date {
actual constructor()
constructor(value: Number)
actual fun getDate(): Int
actual fun getMonth(): Int
actual fun getFullYear(): Int
actual fun getHours(): Int
actual fun getMinutes(): Int
actual fun getTime(): Number
companion object {
fun parse(string: String): Number
}
}
<以下省略>
Kotlin/JS では JavaScript の標準APIである Date
クラスを使って実装をしていますね。
このように共通な Date
クラスを作っておくと、そのクラスは共通なロジック内で使用可能になります。ロジック内で isJVM ? java.util.Date() : isJS ? js.Date() : error
(やべ Kotlin に三項演算子ないんだった)などとプラットフォームごとに分岐しなくても、expect な Date
クラスの actual な実装は、各プラットフォーム向けのビルド時?に置換されます。
では Xamarin ではどうでしょうか?
Xamarin というか .NET の世界では、 Bait and switch というテクニックが一般的なものとして知られています。考え方は Common Module と同じで、「APIが定義されているだけの空っぽのモジュール」を "おとり" にして、「プラットフォーム固有の実装がされた実際のモジュール」 を "喰わせる(=置換する)"、というものです。
- The Bait and Switch PCL Trick
- Plugins for Xamarinを作ろう! - ぴーさんログ
- 共有コードからネイティブ依存処理が使える!PCLを使ったXamarinライブラリ作成テクニック (フェンリル | デベロッパーズブログ)
Xamarin 向けはもちろん、複数プラットフォーム対応をうたう .NET 製ライブラリの多くはこのテクニックを使って作られています。ただし Bait and switch はビルドシステムやIDEのサポートがあるわけではない、ただのファイル置換を利用したtrickなので、 expect
や actual
といったキーワードが用意されている Kotlin のそれの方がより洗練されていると言えます。
共通化するために、ものすごくたくさんの Common Module を作らなければならない?
はい、その通りだと思われます。
マルチプラットフォームで動作可能な API は現在のところ Kotlin の標準API だけだと推測されます(それらも全てが使えるかは分からないですし)。
実際、「日付」という基本的なクラスでさえ、 Common Module を自作しなければならない、まだ始まったばかりの状態ですから、今マルチプラットフォーム Kotlin を採用したらほとんどの機能は Common Module を作成しなければならないでしょう。
今後、一般的なクラスやよく使用される機能が、Kotlin標準APIセットに拡充されて(あるいは別のライブラリが作られて)、マルチプラットフォーム対応が進むと思われます。
そんな 一般的なクラス、よく使われる機能などが体系的にまとめられて、マルチプラットフォームに対応しているのが .NET Standard です。これは、古くは 非Windows向けの.NET Framework である Mono の登場からそれを基盤とした Xamarin や Unity などの隆盛、Linux でも動作する .NET Core のリリースを経て生まれた 共通API セットの仕様 です。
Kotlin がこれから「共通APIセットの仕様」を考えていくなら、いっそ .NET Standard に乗っかってくれればいいのになーと Xamarin 使いとしては思うわけですが、
kotlin標準ライブラリ拡充しなきゃ→仕様考えるの面倒じゃね?→ここに.NET Standardというものがあります→もうこれに沿えばいいんじゃね?→Kotlin .NET のできあがりっ、COOL!
— あめい@ハイドラ待ち (@amay077) 2017年11月4日
って世界線に入る条件を教えて未来のenoさん https://t.co/VMXjHwn2pz
わたしの予知能力では、Java派「iOSでもRoboVMの実装を使い回せば…」Swift派「Swift for AndroidでFoundationを使い回せば…」C# 派「Xamarinなりe4kなりでSystem.*を使えば…」で標準ライブラリ統一戦争になる未来までしか(ry https://t.co/qNVyfXwMlo
— Atsushi Eno (@atsushieno) 2017年11月4日
まあ、 .NET Standard で統一されたらそれこそ .NET のディストピアなので、この混沌の現状こそが我々の望んでいた世界なのかもですね。
開発ツールはどうなるんでしょう?
Android は Android Studio を使うわけですけど、 Kotlin/Native や Kotlin/JS は何をつかうのでしょう? やっぱり IntelliJ IDEA でしょうか。JetBrains としてはここでマネーを得たい考えなのでしょうかねー。
オープンソースプロダクトの開発者さんは、 「JetBrains のOSS開発者向けライセンス」を得るチャンスがあるので、これを機に取得してみてもよいと思います。
( Kotlin/Native の IDE は CLion のようですね。もちろん 現段階の情報 では。)
※JetBrains OpenSource License は、OSS向けのライセンスですので、OSS活動以外での利用はできません、念のため。
「JetBrains のOSS開発者向けライセンス」というか「OSSプロジェクト向けライセンス」なので、OSSプロジェクト以外には使えませんので悪しからず
— 山本 ユースケ (@yusuke) 2017年11月6日
Welcome to Cross-Platform World
ということで Kotlin のマルチプラットフォーム対応についての感想を Xamarin と比べる形で書いてみました。
Xamarin のそれに比べて Kotlin が秀でているのは、
- Kotlin という C# よりもモダンな言語が使用できる
- Kotlin/JS で Webアプリ もカバーする
という点でしょうか。特に後者は Microsoft としては TypeScript を持っているので C# がここに踏み込むことはなさそうです(WebAssembly はあると思う)。 Microsoft 自身も Skype アプリを刷新した際に ReactXP という React/JS ベースのライブラリを使用している理由として「Xamarin は Webアプリの助けにはならないから」と 言って います。
このようにクロスプラットフォーム開発ツールといっても、それぞれ得意分野や可・不可領域があり、また現在できることを把握し、将来性を予想しつつ、共通化コードを最大化したいという欲求と戦う必要があります。
ここまで読まれた方なら言わずもがなですが、Kotlin のマルチプラットフォーム対応でも「iOSの知識が必要ない」なんてことは口が裂けても言えないわけで、個別のプラットフォームの知識を習得した上で、それらを共通化するための手法・テクニックを学び、実戦する必要があります。
ということで、 @AyaseSH さんのツイートをお借りして締めます、「ようこそクロスプラットフォームへ!」。
Kotlin でクロスプラットフォームになったことで (辛いことが共有できる)仲間が増えたとしか思わないなー。ようこそクロスプラットフォームへ。
— オータガー@ざまりんフレンズ (@AyaseSH) 2017年11月4日