August 27, 2019

Kotlin Fest 2019でKotlin Multiplatform Projectについて発表した

2019年8月24日に開催されたKotlin Fest 2019 で、 Kotlin Multiplatform Project入門について話してきました。
(以下Kotlin Multiplatform ProjectをMPPと呼ぶ)

セッション一覧

handbill

(今年のロゴはちょっとシュッとしたらしい)

CFPを出すまでの流れ

去年のKotlin Festは、スピーカーの公募はせずに招待のみで行われていました。
前職のチームでは、Androidでは2015年4月から、サーバサイドKotlinも2016年9月とかなり初期の頃からKotlinを愛でていたKotlin愛に溢れたチームだったので、昨年は弊チームからは先見の明を持つボスが招待されKotlin Festで発表をしていたの見ていました。

来年は何かネタがあれば話したいなとは思いつつ、前職で秋頃に最少人数でそれなりに大きなプロダクトを作り切る方法を考えていた際にKotlin Fest 2018に参加したセッションで丁度MPPの話を聞いて、我々が求めていたのは「これだ」と衝撃が走った記憶が今でもあります。

参加レポートはこちら(前職のブログ)

セッションのYouTubeリンクがHighlightになってる。誰か直して

その後、MPP熱が冷めやらぬ私はDroid Kaigi 2019 のCFPにMPPについてのセッション(出したのはKotlin/Native寄り)を応募しましたが、こちらは残念ながら不採択でした。
当時MPPは日本ではまだ無名で、Kotin/Nativeに関する記事は多少ありましたが、日本語の検索結果は殆どありませんでした。 時代を先取り過ぎたのだ。と気持ちを切り替え、その頃からせっかくなのでこのネタをKotlin Festで話したいなという気持ちを持っていました。

今年のKotlin Fest 2019では初のセッション公募ということで、CFPの募集は今年の5月から6月の間で行われていました。 これは、願ってもないチャンスであったのですが丁度転職をして新しい会社で働くタイミングと、本を執筆していて締切のタイミングが重なり忙しくなるのは目に見えていたので三度くらい躊躇しました。
が、せっかくのチャンスを逃すのは勿体無いので、有給消化中に海外留学をしていたホテルからCFPを書いて締め切り一週間前に応募しました。

ありがたいことに、セッションを採択して頂き怒涛の日々が始まります…

セッションの準備

幸いにも本の締切の方が早かったので、発表内容はある程度固まっていたこともあり、話すスクリプトの内容は初稿と大きなズレは無く、戻りが発生しなかったのは良かったです。
2ヶ月程海外に行っていて、家賃更新のタイミングも重なったのもあり、一人暮らしをしていた家を引き払い実家にいたので、通勤時間が長く 家での作業時間はあまり取れませんでした。そのため資料の8割は通勤の電車の中で作成しています。ちなみにこのブログも通勤の電車の中で書いています。
個人的には電車の中が1番集中出来るので、オフィスにいるよりも捗ります。

スライドは普段 単色を使いがちなのですが、Kotlinのアイコンがグラデーションなので表紙などは珍しくグラデーションを使ったデザインにしてみました。
スライド間のコンテキストを繋げるために、マジックムーブを多様して、それなりに時間をかけたのでスライド自体を褒めて頂けたのは嬉しかったです。

slide

ちなみに、良い感じのスライドに時より見せるダサいスターの演出をすることで逆に気を引くという演出をしたのですが、あまり伝わっていなかったっぽいというのが、懇親会で発覚しましたw
長年スター乞食文化にいたので、GitHubのLinkと星を見たら反射的に「スターしてくれ」とパブロフの犬の如く変換されるのですが意外と世間はそうでもないという事がわかりました。

発表1週間前

スライド自体は1週間前に無事完成して、残りの1週間はスライド自体の修正と、原稿の修正に使いました。
アニメーションを多用するスタイルなので、枚数自体が多くなってしまうのは仕方がないのですが、この時点でスライドの枚数が200枚を超えており、試しに通しで発表練習をしてみたところ55分くらいになってしまったので小ネタ系を削除して、原稿や説明を大幅に削っています。
なので、途中 説明がわかりにくくなってしまった箇所があった点は申し訳ないです。

それでも時間はいっぱいいっぱいだったので、当日余計な事を話さないように、ある程度キッチリ原稿を固めていきました。
また、何回も練習してセクションや要所要所で時間をメモしておき、このスライドに来る段階で何分何十秒のラップタイムじゃないと時間通りに終わらないというのを綿密に決めておきました。

Slide time

33:20というのは、発表開始から33分20秒が経過しているという意味で、16:03:20というのは現在時刻の事を表しています。
Keynoteの発表者ツールを使うと、発表開始からの時間を見ることが出来るのですが、もし何らかのトラブルが発生してスライドが止まった場合に、開始時間からのDiffのみだと残り時間がわからなくなってしまう可能性があるので、自分の発表開始時間15:30からの差分をとって両方表示するようにしていました。

レビュー

幸いにも、今いるチームには前職で一緒にKotlin/Nativeを調べていた や、Kotlin/Nativeに詳しい名前が覚えにくい人Kotlin/Nativeの本 を書いた 等 Kotlinに強い人がたくさんいたので、発表の3日前にマサカリを投げまくって貰ったおかげもあり資料の精度を上げることが出来ました。
話を聞いてくれた社内の方達ありがとうございました。🙏

発表当日

基調講演でSvetlanaさんが盛大にネタバレしてしまって、アッアッとなりましたw
まぁカンファレンスのKeynoteはそんなものなので、聞いてなかったことにして、当初予定していた通りに話しました。

基調講演が終わって昼休みの時間は弊社のブースにいました。
弊社の隣のブースが前職のブースで出戻りはいつですか?と声をかけて頂けたのは嬉しかったです。
所属会社は、ただのタイミングなので人材流動していきたいですね。 ブースで寿司打やったりしていて自由な感じが好きです。
私には緑色のイメージがあるっぽく、いろいろな人に青に違和感があると言われましたw

発表前

昼休み後のセッションからは、発表者控室で最後の調整をしていました。
サンプルのリポジトリ とかはまだpushしてなかったので、この時に用意していました。

この控室はとても素晴らしくて、去年もスポンサーとしてお邪魔していたので知っていたのですが、セッションの動画が中継されているので、準備しながらセッションを見ることが出来ます。

Waiting room

DroidKaigiの時にやっていたのですが、せっかく同じ時間にいくつも素晴らしいセッションがあるのに、いざ始まったら思っていた内容と違うという聞く側のミスマッチを防げるので、発表前の休憩時間に表示する用の概要スライドを用意していたのですが、今回はセッション前に画面を映せなかったのでTweetすることにしました。

abstract

ただ、これだとどうしても全ての人に届けるのが難しいので、頼んで表示させて貰っても良かったかもしれないです。

基本的には、せっかく土曜日にわざわざ話を聞きに来てくれているので、その人達の時間を無駄にしたくはないなという考えが根本にあります。

発表後

何度も練習した甲斐あって、発表時間は45分00秒ピッタリに終わったみたいです。
司会の運営の方達から時間ピッタリでしたとフィードバックを頂きました。
途中ラップタイムを下回っていた箇所は結構早口で話して挽回するみたいな調整をしていました。

普段あまりしないのですが、今回はそれなりに時間をかけた発表だったのでエゴサをしましたw
疑問に感じている箇所をTweetしている方達に返信をしたり、セッションのフィードバックに対していいねをしましたw
圧をかけてスミマセンw

終わってからは余裕が出来たので、Kotlin/JSとKotlin/Nativeのセッションを聞きました。
Kotlin/Nativeで発表していた荻野くんは、前職の新卒でもし私が辞めていなかったら同じチームでMPPをやりたいと希望してくれていたみたいで申し訳ないです。

発表したスライドはこちら
200枚ぐらいあるので、後日動画を見たほうがわかりやすそうです

スライド供養

時間の都合で漏れたスライド達を一部供養しておきます。
もちろん墓場でも供養してもらえないスライドたちがたくさんいる…

plugin
test
dce

セッションでも少し解説をしたが、Kotlin/JSは依存関係に当然Kotlin/JSのコードが必要になるので、 そのままだとjsのファイルサイズがどうしても大きくなってしまいます。
kotlin-dce plugin は参照していないKotlin/JSのコードを削除してくれるので、これを使ってファイルのサイズを削減します。AndroidのProGuardをかけた時と同じような動作をします。
JSのminifyではないので、さらにminifyする必要もあります。

あと、発表ではKotlin/NativeのiOS読み込み部分を解説していません。
他のプラットフォームは全部Gradleで解説出来るのですが、iOSだけXCode側の設定になるので止む無く削減しました。
iOSの設定は少し古いのですが、この辺を参考にすると良いと思います。(動作未確認)

後述の本をお願いします…

質問回答

Resultは使えないの?

Ask the speakerで kotlin.Rsultは使えないの?という質問を頂きました。
確かに、と思って調べてみました。

  • Commonコードです

runCatchingを使うことで、ネストが一段階減りシンプルになりました。
今回みたいに、エラーが発生した際に特にやることがなければ、runCatchingで良さそうです。

// Before
fun getGreeting(successCallback: (Greeting) -> Unit, errorCallback: (Exception) -> Unit) {
    GlobalScope.apply {
        launch(coroutineDispatcher) {
            try {
                val result = httpClient.get<String> {
                    url {
                        protocol = URLProtocol.HTTP
                        host = hostName
                        port = 8080
                    }
                }
                val greeting = Json.parse(Greeting.serializer(), result)
                successCallback(greeting)
            } catch (e: Exception) {
                errorCallback(e)
            }
        }
    }
}

// After: Result
fun getGreeting(onResult: (Result<Greeting>) -> Unit) {
    GlobalScope.apply {
        launch(coroutineDispatcher) {
            onResult(runCatching {
                val result = httpClient.get<String> {
                    url {
                        protocol = URLProtocol.HTTP
                        host = hostName
                        port = 8080
                    }
                }
                Json.parse(Greeting.serializer(), result)
            })
        }
    }
}
  • Kotlin(Android)

AndroidはKotlinコードなので、普通に問題なく使えます。

// Before
ApiClient().getGreeting(
    successCallback = {
       handler.post { helloText.text = it.hello }
    },
    errorCallback = {
       handler.post { helloText.text = it.toString() }
    })

// After: Result
ApiClient().getGreeting { result ->
    result.onSuccess {
        handler.post { helloText.text = it.hello }
    }
    result.onFailure {
        handler.post { helloText.text = it.toString() }
    }
}
  • Swift(iOS)

結果から言うと、iOSは駄目でした。
Swfitは、Resultの型がAny?になって返ってきてしまいます。
Castしてあげれば使えなくは無いと思いますが、現状そこまでメリットは無さそうです。

// Before
ApiClient().getGreeting(
    successCallback:{ response in
        label.text = response.hello
    }, errorCallback: { error in
        print(error)
    }
)

// After: Result
ApiClient().getGreeting { response in
   response // Any
}

Resultクラスはこの様に定義されています。

@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS")
@SinceKotlin("1.3")
public inline class Result<out T> @PublishedApi internal constructor(
    @PublishedApi
    internal val value: Any?
) : Serializable {
  ...
}

なぜ、こうなってしまうのかというと、セッションでも説明しましたが、現状Kotlin/Nativeではinlineクラスに対応していません。
そのため、Swift側ではAnyで返ってきてしまいます。

Kotlin 1.3.40から、-Xobjc-generics というオプションが追加されて、Kotlin/NativeでのGenericsの問題が解消されたので、Resultのinlineを外して試しにやってみました。

@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS")
@SinceKotlin("1.3")
class MyResult<out T> internal constructor(
        val value: Any?
) {

    val isSuccess: Boolean get() = value !is Failure

    val isFailure: Boolean get() = value is Failure

    fun exceptionOrNull(): Throwable? =
            when (value) {
                is Failure -> value.exception
                else -> null
            }

    /**
     * Companion object for [Result] class that contains its constructor functions
     * [success] and [failure].
     */
    companion object {
        fun <T> success(value: T): MyResult<T> =
                MyResult(value)

        fun <T> failure(exception: Throwable): MyResult<T> =
                MyResult(createFailure(exception))
    }

    internal class Failure(
            val exception: Throwable
    ) {
        override fun equals(other: Any?): Boolean = other is Failure && exception == other.exception
        override fun hashCode(): Int = exception.hashCode()
        override fun toString(): String = "Failure($exception)"
    }
}

@PublishedApi
@SinceKotlin("1.3")
internal fun createFailure(exception: Throwable): Any = MyResult.Failure(exception)

@SinceKotlin("1.3")
inline fun <T, R> T.myRunCatching(block: T.() -> R): MyResult<R> {
    return try {
        MyResult.success(block())
    } catch (e: Throwable) {
        MyResult.failure(e)
    }
}

@UseExperimental(ExperimentalContracts::class)
@SinceKotlin("1.3")
inline fun <reified T : Any> MyResult<T>.onSuccess(action: (value: T) -> Unit): MyResult<T> {
    contract {
        callsInPlace(action, InvocationKind.AT_MOST_ONCE)
    }
    if (isSuccess) action(value as T)
    return this
}

fun <T> MyResult<T>.onFailure(action: (exception: Throwable) -> Unit): MyResult<T> {
    exceptionOrNull()?.let { action(it) }
    return this
}
ApiClient().getGreeting { response in
    response.onSuccess(action: { result in // resultがAnyになってしまう
       let greeting = result as? Greeting
       label.text = greeting?.hello
    })
    response.onFailure(action: { error in
        print(error)
    })
}

responseの段階ではMyResult<Greeting>となっているのですが、onSuccessの段階でAnyに変換されてしまいました。
書いたonSuccessのコードが良くない可能性もありますが、現段階では使うことは出来なさそうでした。
この部分は、長くなってしまったので、引き続き調査を続けたいと思います。

まとめ

45分の発表なので準備が結構大変でしたが、参加出来て良かったです!
スタッフの方達ありがとうございました。 (隣と隣の席の人が運営スタッフなので、開催日の週は大変そうにしていました)
今回の発表は、MPPがまだあまり知られていない日本で、僕がKotlin Festの会場で受けた衝撃を伝えるという目標があったので、このツイートは1番嬉しかったです。
知らないことを知るというカンファレンスの醍醐味に少しでも力になれていれば嬉しいです。

宣伝

book

セッションの最後でも話しましたが、今年の秋頃に本が出ます。👏🏻
イメージ的には、みんなのGo のような、Kotlin in Action等の入門本を終えて実践で使っていく人達向けに解説している本になります。
僕はMPPの部分を書きました。 まだそこまでネタ自体が無いので、大枠はセッションと同じ内容ですが、話しきれなかった細かい部分にも多少触れているのと、サンプル作成のStep by Stepの部分がもう少し詳しく説明しています。
他にも、AndroidやServer, テスト, Coroutineの章があるので、興味がある人は是非宜しくお願い致します!!
最高のカンファレンスでした!

Have a nice MPP

© AAkira 2023