Xamarin 使いが Kotlin のマルチプラットフォーム対応コードを読んだ感想

  • 119
    Like
  • 4
    Comment

Kotlin のマルチプラットフォーム対応、アツいですね。

上の 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つ。

  1. Kotlin/Native による iOS ネイティブアプリのビルドが可能
  2. "Common Module" と呼ばれる手法で、プラットフォーム毎の処理を共通化できる

これらについて、 Xamarin と比較しながら見ていきましょうか。

1. Kotlin/Native による iOS ネイティブアプリのビルドが可能

これは LLVM 技術を利用して、Kotlin のソースコードから iOS のネイティブバイナリを出力する、というものです。LLVM を使用していることから、iOS 以外にも対応可能とされています。

Kotlin/Native(iOS)のソースコード からは、 AppDelegateUIResponder などの 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()}"

getDateparseDate など、ありがちなメソッドが定義されていますが、ここに実装はなく、代わりに見慣れないキーワード 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.Datejava.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が定義されているだけの空っぽのモジュール」を "おとり" にして、「プラットフォーム固有の実装がされた実際のモジュール」 を "喰わせる(=置換する)"、というものです。

Xamarin 向けはもちろん、複数プラットフォーム対応をうたう .NET 製ライブラリの多くはこのテクニックを使って作られています。ただし Bait and switch はビルドシステムやIDEのサポートがあるわけではない、ただのファイル置換を利用したtrickなので、 expectactual といったキーワードが用意されている 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 使いとしては思うわけですが、

まあ、 .NET Standard で統一されたらそれこそ .NET のディストピアなので、この混沌の現状こそが我々の望んでいた世界なのかもですね。

開発ツールはどうなるんでしょう?

Android は Android Studio を使うわけですけど、 Kotlin/Native や Kotlin/JS は何をつかうのでしょう? やっぱり IntelliJ IDEA でしょうか。JetBrains としてはここでマネーを得たい考えなのでしょうかねー。

オープンソースプロダクトの開発者さんは、 「JetBrains のOSS開発者向けライセンス」を得るチャンスがあるので、これを機に取得してみてもよいと思います。

( Kotlin/Native の IDE は CLion のようですね。もちろん 現段階の情報 では。)

※JetBrains OpenSource License は、OSS向けのライセンスですので、OSS活動以外での利用はできません、念のため。

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 さんのツイートをお借りして締めます、「ようこそクロスプラットフォームへ!」。