Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DroidKaigiアプリの Kotlin Multiplatform

takahirom
March 27, 2019

DroidKaigiアプリの Kotlin Multiplatform

takahirom

March 27, 2019
Tweet

More Decks by takahirom

Other Decks in Programming

Transcript

  1. DroidKaigiアプリの
    Kotlin Multiplatform
    takahirom

    View Slide

  2. About me
    • Androidが好きです
    • AbemaTVのAndroidアプリを作っています。
    • DroidKaigi 2018, 2019公式アプリのOwnerをしました。
    takahirom
    (@new_runnable)
    @takahirom
    @takahirom

    View Slide

  3. DroidKaigi 公式アプリ

    View Slide

  4. DroidKaigi 公式アプリ
    Android版 iOS版

    View Slide

  5. DroidKaigiアプリのiOS版を
    どうするか問題

    View Slide

  6. DroidKaigiアプリのiOS版を
    どうするか問題
    • DroidKaigiでもiPhoneを使っている⼈が意外と多い。
    • 去年はFlutterでiOS版作ってくれた⽅もいたが、

    今年はどうするか。

    View Slide

  7. Kotlin Multiplatformを
    導⼊しやすい構成にする

    View Slide

  8. Kotlin Multiplatformを導⼊しやすい構成にする 1
    Ktor-Client + kotlinx.serializationを使う

    View Slide

  9. Kotlin Multiplatformを導⼊しやすい構成にする 1
    Ktor-Client + kotlinx.serializationを使う
    • Androidで定番のHTTP APIの呼び出し処理を⽣成してくれ
    るRetrofitはKotlin Multiplatformに対応していない

    View Slide

  10. Kotlin Multiplatformを導⼊しやすい構成にする 1
    Ktor-Client + kotlinx.serializationを使う
    • Androidで定番のHTTP APIの呼び出し処理を⽣成してくれ
    るRetrofitはKotlin Multiplatformに対応していない
    • → Ktor-ClientというKotlin Multiplatformで使えるHTTP ク
    ライアントを使う。
    https://github.com/ktorio/ktor より

    View Slide

  11. Kotlin Multiplatformを導⼊しやすい構成にする 1
    Ktor-Client + kotlinx.serializationを使う

    View Slide

  12. Kotlin Multiplatformを導⼊しやすい構成にする 1
    Ktor-Client + kotlinx.serializationを使う
    • 同様にgsonやmoshiではなく、Kotlin/kotlinx.serialization
    というjsonオブジェクトマッパーを使う

    View Slide

  13. Kotlin Multiplatformを導⼊しやすい構成にする 1
    Ktor-Client + kotlinx.serializationを使う
    • 同様にgsonやmoshiではなく、Kotlin/kotlinx.serialization
    というjsonオブジェクトマッパーを使う
    @Serializable
    data class SponsorItemResponseImpl(
    override val id: Int,
    override val name: String,
    override val url: String,
    override val image: String
    ) : SponsorItemResponse
    アノテーションを付ける

    View Slide

  14. Kotlin Multiplatformを導⼊しやすい構成にする 1
    Ktor-Client + kotlinx.serializationを使う
    コードのイメージ

    View Slide

  15. Kotlin Multiplatformを導⼊しやすい構成にする 1
    Ktor-Client + kotlinx.serializationを使う
    override suspend fun getSessions(): Response {
    val rawResponse = httpClient.get {
    url("$apiEndpoint/timetable")
    accept(ContentType.Application.Json)
    }
    return Json.nonstrict.parse(ResponseImpl.serializer(), rawResponse)
    }
    コードのイメージ

    View Slide

  16. Kotlin Multiplatformを導⼊しやすい構成にする 1
    Ktor-Client + kotlinx.serializationを使う
    override suspend fun getSessions(): Response {
    val rawResponse = httpClient.get {
    url("$apiEndpoint/timetable")
    accept(ContentType.Application.Json)
    }
    return Json.nonstrict.parse(ResponseImpl.serializer(), rawResponse)
    }
    (使い勝⼿はRetrofitのほうがいいです)
    コードのイメージ

    View Slide

  17. Kotlin Multiplatformを導⼊しやすい構成にする 2
    ⾮同期処理はKotlin Coroutinesを使う

    View Slide

  18. Kotlin Multiplatformを導⼊しやすい構成にする 2
    ⾮同期処理はKotlin Coroutinesを使う
    • Androidで定番の⾮同期処理ライブラリのRxJavaなど
    はMultiplatform⾮対応。

    View Slide

  19. Kotlin Multiplatformを導⼊しやすい構成にする 2
    ⾮同期処理はKotlin Coroutinesを使う
    • Androidで定番の⾮同期処理ライブラリのRxJavaなど
    はMultiplatform⾮対応。
    • Kotlin coroutinesはKotlin/Nativeからも使える。

    View Slide

  20. Kotlin Multiplatformを導⼊しやすい構成にする 2
    ⾮同期処理はKotlin Coroutinesを使う
    • Androidで定番の⾮同期処理ライブラリのRxJavaなど
    はMultiplatform⾮対応。
    • Kotlin coroutinesはKotlin/Nativeからも使える。
    suspend fun getStaffs(): StaffResponse

    View Slide

  21. Kotlin Multiplatformを導⼊しやすい構成にする 2
    ⾮同期処理はKotlin Coroutinesを使う
    • Androidで定番の⾮同期処理ライブラリのRxJavaなど
    はMultiplatform⾮対応。
    • Kotlin coroutinesはKotlin/Nativeからも使える。
    suspend fun getStaffs(): StaffResponse

    View Slide

  22. Kotlin Multiplatformを導⼊しやすい構成にする 3
    Modelで使うクラスはKotlin Multiplatformで使えるも
    のだけにする
    @AndroidParcelize
    data class SpeechSession(
    override val id: String,
    override val dayNumber: Int,
    override val startTime: DateTime,
    override val endTime: DateTime,

    : AndroidParcel {
    DroidKaigiの1つのセッションを表すクラス

    View Slide

  23. Kotlin Multiplatformを導⼊しやすい構成にする 3
    Modelで使うクラスはKotlin Multiplatformで使えるも
    のだけにする
    @AndroidParcelize
    data class SpeechSession(
    override val id: String,
    override val dayNumber: Int,
    override val startTime: DateTime,
    override val endTime: DateTime,

    : AndroidParcel {
    継承元interfaceを
    platform毎に変更することで
    AndroidのParcelableを
    Kotlin Multiplatformでも
    利⽤できるようにする

    View Slide

  24. Kotlin Multiplatformを導⼊しやすい構成にする 3
    Modelで使うクラスはKotlin Multiplatformで使えるも
    のだけにする
    @AndroidParcelize
    data class SpeechSession(
    override val id: String,
    override val dayNumber: Int,
    override val startTime: DateTime,
    override val endTime: DateTime,

    : AndroidParcel {
    継承元interfaceを
    platform毎に変更することで
    AndroidのParcelableを
    Kotlin Multiplatformでも
    利⽤できるようにする
    詳しくは1つ前の発表のAAkiraさんの以下の記事が最⾼です
    Kotlin Multiplatform環境でKotlin SerializationとAndroid
    ExtensionsのParcelize Annotationを使う
    actual interface AndroidParcel : Parcelable
    • androidMain内

    View Slide

  25. Kotlin Multiplatformを導⼊しやすい構成にする 3
    Modelで使うクラスはKotlin Multiplatformで使えるも
    のだけにする
    @AndroidParcelize
    data class SpeechSession(
    override val id: String,
    override val dayNumber: Int,
    override val startTime: DateTime,
    override val endTime: DateTime,

    AndroidParcel {
    ← Klockという
     Kotlin Multiplatformで
     使える⽇付や時間の
     ライブラリを使う

    View Slide

  26. Kotlin Multiplatformを導⼊しやすい構成にする 3
    Modelで使うクラスはKotlin Multiplatformで使えるも
    のだけにする
    @AndroidParcelize
    data class SpeechSession(
    override val id: String,
    override val dayNumber: Int,
    override val startTime: DateTime,
    override val endTime: DateTime,

    AndroidParcel {
    ← Klockという
     Kotlin Multiplatformで
     使える⽇付や時間の
     ライブラリを使う
    今後はJetbrainsが⽇付などのライブラリを
    開発予定らしい

    View Slide

  27. 開発初期に想定した構成

    View Slide

  28. 開発初期に想定した構成
    "OESPJE.PEVMFT
    BQJ
    NPEFM
    Kotlin Multi Platform Project
    J041SPKFDU

    View Slide

  29. あとはissue⽴てて放置

    View Slide

  30. kikuchyさんによる
    Kotlin Multiplatform
    Module化

    View Slide

  31. 問題が発⽣

    View Slide

  32. 初期開発時の想定
    "OESPJE.PEVMFT
    BQJ
    NPEFM
    Kotlin Multi Platform Project
    J041SPKFDU

    View Slide

  33. 初期開発時の想定
    "OESPJE.PEVMFT
    BQJ
    NPEFM
    Kotlin Multi Platform Project
    J041SPKFDU
    2つ以上のModuleは
    読み込めない

    View Slide

  34. DroidKaigiの構成 

    (Kotlin MPP特化版)
    "OESPJE.PEVMFT
    BQJ
    NPEFM
    Kotlin Multi Platform Project
    JPTDPNCJOFE
    J041SPKFDU
    kotlin.srcDirsで

    他のモジュールを

    参照する

    View Slide

  35. この構成も問題が

    View Slide

  36. Android Studio 3.4で
    same content rootのエラーが出る

    View Slide

  37. Android Studio 3.4で
    same content rootのエラーが出る
    同じコードが複数のモジュールで使われるため?
    Android Studio上でエラーが表⽰される

    View Slide

  38. ios-combinedモジュールをAndroidの
    開発時にはsettings.gradleからコメントアウト
    "OESPJE.PEVMFT
    BQJ
    NPEFM
    Kotlin Multi Platform Project
    JPTDPNCJOFE
    J041SPKFDU
    ← コメントアウト

    View Slide

  39. ios-combinedモジュールをAndroidの
    開発時にはsettings.gradleからコメントアウト
    "OESPJE.PEVMFT
    BQJ
    NPEFM
    Kotlin Multi Platform Project
    JPTDPNCJOFE
    J041SPKFDU
    ← iOSの⼈には
    ⼿動でアンコメント
    してもらう

    View Slide

  40. ios-combinedモジュールをAndroidの
    開発時にはsettings.gradleからコメントアウト
    "OESPJE.PEVMFT
    BQJ
    NPEFM
    Kotlin Multi Platform Project
    JPTDPNCJOFE
    J041SPKFDU
    ← iOSの⼈には
    ⼿動でアンコメント
    してもらう

    View Slide

  41. 現状は1 common moduleが無難み
    たい
    (解決策知っていたら教えてください)

    View Slide

  42. Kotlin Multiplatformと
    Dagger

    View Slide

  43. DroidKaigiのAPIのモジュール
    • それぞれでコードが分かれている

    View Slide

  44. DroidKaigiのAPIのモジュール
    Androidから使われる部分だけ、
    Daggerが使えるので、
    うまく継承したものを@Provide
    して頑張る。

    View Slide

  45. DroidKaigiのAPIのモジュール
    @Binds abstract fun DroidKaigiApi(impl: InjectableKtorDroidKaigiApi): DroidKaigiApi
    • main/の中では普通にAndroidのコードが書けるので、

    うまく継承したりしてInjectしてあげる
    class InjectableKtorDroidKaigiApi @Inject constructor(
    httpClient: HttpClient,
    @Named("apiEndpoint") apiEndpoint: String
    ) : KtorDroidKaigiApi(httpClient, apiEndpoint, null)

    View Slide

  46. Kotlin Multiplatformと
    Kotlin Coroutines

    View Slide

  47. iOSのSwiftのコードから
    Kotlinのsuspend functionが
    呼べない

    View Slide

  48. iOSのSwiftのコードから
    Kotlinのsuspend functionが
    呼べない問題
    override fun getSessions(
    callback: (response: Response) -> Unit,
    onError: (error: Exception) -> Unit
    ) {
    GlobalScope.launch(requireNotNull(coroutineDispatcherForCallback)) {
    try {
    val response = getSessions()
    callback(response)
    } catch (ex: Exception) {
    onError(ex)
    }
    }
    }

    Kotlin Coroutinesのコードをコールバックを使って
    ラップするメソッドを作成
    成功時と失敗時のコールバック

    View Slide

  49. iOSのSwiftのコードから
    Kotlinのsuspend functionが
    呼べない問題
    override fun getSessions(
    callback: (response: Response) -> Unit,
    onError: (error: Exception) -> Unit
    ) {
    GlobalScope.launch(requireNotNull(coroutineDispatcherForCallback)) {
    try {
    val response = getSessions()
    callback(response)
    } catch (ex: Exception) {
    onError(ex)
    }
    }
    }

    Kotlin Coroutinesのコードをコールバックを使って
    ラップするメソッドを作成
    Coroutinesをlaunch

    View Slide

  50. iOSのSwiftのコードから
    Kotlinのsuspend functionが
    呼べない問題
    override fun getSessions(
    callback: (response: Response) -> Unit,
    onError: (error: Exception) -> Unit
    ) {
    GlobalScope.launch(requireNotNull(coroutineDispatcherForCallback)) {
    try {
    val response = getSessions()
    callback(response)
    } catch (ex: Exception) {
    onError(ex)
    }
    }
    }

    Kotlin Coroutinesのコードをコールバックを使って
    ラップするメソッドを作成
    既存のsuspend functionを呼び出す

    View Slide

  51. iOSのSwiftのコードから
    Kotlinのsuspend functionが
    呼べない問題
    • この問題にぬっかさんが対応策を⾒つけました

    View Slide

  52. iOSのSwiftのコードから
    Kotlinのsuspend functionが
    呼べない問題
    override fun getSessionsAsync(): Deferred =
    GlobalScope.async(requireNotNull(coroutineDispatcherForCallback)) {
    getSessions()
    }
    Kotlin Multiplatform Module側に

    Deferredを返すメソッドを作っておく

    View Slide

  53. iOSのSwiftのコードから
    Kotlinのsuspend functionが
    呼べない問題
    override fun getSessionsAsync(): Deferred =
    GlobalScope.async(requireNotNull(coroutineDispatcherForCallback)) {
    getSessions()
    }
    Kotlin Multiplatform Module側に

    Deferredを返すメソッドを作っておく
    SwiftでDeferredをasSingle()でRxSwiftのSingleに変換する
    func fetch() -> Single {
    return ApiComponentKt.generateDroidKaigiApi()
    .getSessionsAsync()
    .asSingle(Response.self)
    .map { ResponseToModelMapperKt.toModel($0) }
    .catchError { throw handledKotlinException($0) }

    View Slide

  54. iOSのSwiftのコードから
    Kotlinのsuspend functionが
    呼べない問題
    override fun getSessionsAsync(): Deferred =
    GlobalScope.async(requireNotNull(coroutineDispatcherForCallback)) {
    getSessions()
    }
    Kotlin Multiplatform Module側に

    Deferredを返すメソッドを作っておく
    SwiftでDeferredをasSingle()でRxSwiftのSingleに変換する
    func fetch() -> Single {
    return ApiComponentKt.generateDroidKaigiApi()
    .getSessionsAsync()
    .asSingle(Response.self)
    .map { ResponseToModelMapperKt.toModel($0) }
    .catchError { throw handledKotlinException($0) }
    asSingle()の中⾝は?

    View Slide

  55. iOSのSwiftのコードから
    Kotlinのsuspend functionが
    呼べない問題
    extension Kotlinx_coroutines_core_nativeDeferred {
    func asSingle(_ elementType: ElementType.Type) ->
    Single {
    return Single.create { observer in
    self.invokeOnCompletion { cause in
    if let cause = cause {
    observer(.error(cause))
    return KotlinUnit()
    }
    if let result = self.getCompleted() as? ElementType {
    observer(.success(result))
    return KotlinUnit()
    }
    • asSingle()はKotlinx_coroutines_core_nativeDeferredに対する
    extension functionになっている

    View Slide

  56. iOSのSwiftのコードから
    Kotlinのsuspend functionが
    呼べない問題
    extension Kotlinx_coroutines_core_nativeDeferred {
    func asSingle(_ elementType: ElementType.Type) ->
    Single {
    return Single.create { observer in
    self.invokeOnCompletion { cause in
    if let cause = cause {
    observer(.error(cause))
    return KotlinUnit()
    }
    if let result = self.getCompleted() as? ElementType {
    observer(.success(result))
    return KotlinUnit()
    }
    Deferred#invokeOnCompletionを呼び出すことで
    Coroutinesを起動できる
    • asSingle()はKotlinx_coroutines_core_nativeDeferredに対する
    extension functionになっている

    View Slide

  57. Kotlin Multiplatformと
    Dynamic feature module

    View Slide

  58. Dynamic feature module
    • 機能を利⽤される直前にダウンロードすることで、ダウンロード

    容量を削減できるAndroidの機能。
    • STAR-ZEROさんという⽅から最⾼のPRがいただけました。

    https://github.com/DroidKaigi/conference-app-2019/pull/637
    • (かなりシンプルな内容で、Dagger関連の事情をクリアしていた。)

    View Slide

  59. Kotlin Multiplatformと
    Dynamic feature moduleを
    組み合わせるとリリースビルド出来ない

    View Slide

  60. Dynamic feature moduleで
    リリースビルド出来ない
    hogehoge.kotlin_moduleというクラスが、

    ビルド途中で重複してしまうために起こるようです。
    現状、Workaroundがありません。
    • https://github.com/DroidKaigi/conference-app-2019/issues/180

    View Slide

  61. Dynamic feature moduleで
    リリースビルド出来ない
    この問題はサンプルプロジェクトを作って報告して

    修正待ちという形です。

    View Slide

  62. APIの環境の切り替え

    View Slide

  63. APIの環境の切り替え
    iOSもあるので、AndroidのBuildConfigだけではダメ。

    View Slide

  64. APIの環境の切り替え
    iOSもあるので、AndroidのBuildConfigだけではダメ。
    internal expect fun apiEndpoint(): String
    common

    View Slide

  65. APIの環境の切り替え
    iOSもあるので、AndroidのBuildConfigだけではダメ。
    internal expect fun apiEndpoint(): String
    common
    Android
    internal actual fun apiEndpoint(): String = BuildConfig.API_ENDPOINT

    View Slide

  66. APIの環境の切り替え
    iOSもあるので、AndroidのBuildConfigだけではダメ。
    internal expect fun apiEndpoint(): String
    common
    Android
    internal actual fun apiEndpoint(): String = BuildConfig.API_ENDPOINT
    iOS
    data/api-impl/src/iosMain/kotlinDebug/ /ApiEndpoint.kt
    internal actual fun apiEndpoint(): String = “https://.../api”
    data/api-impl/src/iosMain/kotlinRelease/ /ApiEndpoint.kt
    internal actual fun apiEndpoint(): String = “https://.../api”

    View Slide

  67. APIの環境の切り替え
    ⼀応環境の切り替えは実現できているがURLを⼆箇所に書いて
    いるので、うまく出来ていない
    せーいさんという⽅のこのライブラリを使えばうまくできるか
    も?
    https://github.com/yshrsmz/BuildKonfig

    View Slide

  68. ハマったポイント

    View Slide

  69. Kotlin/Nativeの
    対応Architectureによる
    iOSでのリリースの制限

    View Slide

  70. Undefined symbols for
    architecture armv7
    iOSでのリリース作業中に発覚した問題として、以下がありました
    ⼀応新しいiPhoneでは⼤丈夫のようですが、古いiPhoneでは
    インストール出来ないみたいです。
    https://github.com/JetBrains/kotlin-native/issues/1460

    View Slide

  71. Android Studio上で
    Kotlin Multiplatform Moduleのク
    ラスがUnresolved referenceになる

    View Slide

  72. Android Studio上で
    Kotlin Multiplatform ModuleのクラスがUnresolved
    referenceになる
    基本的には
    ・enableFeaturePreview(“GRADLE_METADATA")
    をsettings.gradleに記述
    (これはGradle 6.0からデフォルトになるらしい。)
    ・Gradleのバージョンを4.7にすることで解決。

    View Slide

  73. Android Studio上で
    Kotlin Multiplatform ModuleのクラスがUnresolved
    referenceになる
    基本的には
    ・enableFeaturePreview(“GRADLE_METADATA")
    をsettings.gradleに記述
    (これはGradle 6.0からデフォルトになるらしい。)
    ・Gradleのバージョンを4.7にすることで解決。
    GRADLE_METADATAとは?

    View Slide

  74. Gradle Module Metadataとは
    参考 Introducing Gradle Module Metadataより
    https://blog.gradle.org/gradle-metadata-1.0
    • Gradle 5.3で1.0になった機能。
    • .moduleで終わるjsonのファイル
    • 例えば今までのJavaのバージョンが

    pomファイルから分からなかったが、

    .moduleで分かるようになったりする。
    • Kotlin/Nativeでは別々のアーキテクチャによって

    別のバイナリを使ったりできる

    View Slide

  75. Kotlin Multiplatform
    を使ってみてどうだったか?

    View Slide

  76. Kotlin Multiplatform
    を使ってみて
    • いろいろな問題はありましたが、

    普通にKotlinで書かれたクラスやメソッドなどが使えて共通化して

    いけたのは最⾼の体験でした。
    • 普通にXCodeで補完も効いて

    キャストもうまく動いていい感じでした

    View Slide

  77. まとめ
    さまざまな⼈の⼒によりDroidKaigiのKotlin Multiplatformの
    実装がされていて、その知⾒の紹介をしました。
    • Kotlin Multiplatformを導⼊しやすい構成とは
    • Multi Moduleとの組み合わせ
    • Daggerとの組み合わせ
    • Kotlin Coroutinesとの組み合わせ
    • Dynamic feature moduleとの組み合わせ

    View Slide

  78. ありがとうございました

    View Slide