DroidKaigiアプリの Kotlin Multiplatform

7166bc2cbc462ab5fd1987a76d0fe709?s=47 takahirom
March 27, 2019

DroidKaigiアプリの Kotlin Multiplatform

7166bc2cbc462ab5fd1987a76d0fe709?s=128

takahirom

March 27, 2019
Tweet

Transcript

  1. DroidKaigiアプリの Kotlin Multiplatform takahirom

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

    takahirom (@new_runnable) @takahirom @takahirom
  3. DroidKaigi 公式アプリ

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

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

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

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

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

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

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

    Multiplatformに対応していない • → Ktor-ClientというKotlin Multiplatformで使えるHTTP ク ライアントを使う。 https://github.com/ktorio/ktor より
  11. Kotlin Multiplatformを導⼊しやすい構成にする 1 Ktor-Client + kotlinx.serializationを使う

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

  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 アノテーションを付ける
  14. Kotlin Multiplatformを導⼊しやすい構成にする 1 Ktor-Client + kotlinx.serializationを使う コードのイメージ

  15. Kotlin Multiplatformを導⼊しやすい構成にする 1 Ktor-Client + kotlinx.serializationを使う override suspend fun getSessions():

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

    Response { val rawResponse = httpClient.get<String> { url("$apiEndpoint/timetable") accept(ContentType.Application.Json) } return Json.nonstrict.parse(ResponseImpl.serializer(), rawResponse) } (使い勝⼿はRetrofitのほうがいいです) コードのイメージ
  17. Kotlin Multiplatformを導⼊しやすい構成にする 2 ⾮同期処理はKotlin Coroutinesを使う

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

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

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

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

    coroutinesはKotlin/Nativeからも使える。 suspend fun getStaffs(): StaffResponse
  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つのセッションを表すクラス
  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でも 利⽤できるようにする
  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内
  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で  使える⽇付や時間の  ライブラリを使う
  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が⽇付などのライブラリを 開発予定らしい
  27. 開発初期に想定した構成

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

  29. あとはissue⽴てて放置

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

  31. 問題が発⽣

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

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

    読み込めない
  34. DroidKaigiの構成 
 (Kotlin MPP特化版) "OESPJE.PEVMFT BQJ NPEFM Kotlin Multi Platform

    Project JPTDPNCJOFE J041SPKFDU kotlin.srcDirsで
 他のモジュールを
 参照する
  35. この構成も問題が

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

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

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

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

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

    J041SPKFDU ← iOSの⼈には ⼿動でアンコメント してもらう
  41. 現状は1 common moduleが無難み たい (解決策知っていたら教えてください)

  42. Kotlin Multiplatformと Dagger

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

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

  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)
  46. Kotlin Multiplatformと Kotlin Coroutines

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

  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のコードをコールバックを使って ラップするメソッドを作成 成功時と失敗時のコールバック
  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
  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を呼び出す
  51. iOSのSwiftのコードから Kotlinのsuspend functionが 呼べない問題 • この問題にぬっかさんが対応策を⾒つけました

  52. iOSのSwiftのコードから Kotlinのsuspend functionが 呼べない問題 override fun getSessionsAsync(): Deferred<Response> = GlobalScope.async(requireNotNull(coroutineDispatcherForCallback))

    { getSessions() } Kotlin Multiplatform Module側に
 Deferredを返すメソッドを作っておく
  53. iOSのSwiftのコードから Kotlinのsuspend functionが 呼べない問題 override fun getSessionsAsync(): Deferred<Response> = GlobalScope.async(requireNotNull(coroutineDispatcherForCallback))

    { getSessions() } Kotlin Multiplatform Module側に
 Deferredを返すメソッドを作っておく SwiftでDeferredをasSingle()でRxSwiftのSingleに変換する func fetch() -> Single<SessionContents> { return ApiComponentKt.generateDroidKaigiApi() .getSessionsAsync() .asSingle(Response.self) .map { ResponseToModelMapperKt.toModel($0) } .catchError { throw handledKotlinException($0) }
  54. iOSのSwiftのコードから Kotlinのsuspend functionが 呼べない問題 override fun getSessionsAsync(): Deferred<Response> = GlobalScope.async(requireNotNull(coroutineDispatcherForCallback))

    { getSessions() } Kotlin Multiplatform Module側に
 Deferredを返すメソッドを作っておく SwiftでDeferredをasSingle()でRxSwiftのSingleに変換する func fetch() -> Single<SessionContents> { return ApiComponentKt.generateDroidKaigiApi() .getSessionsAsync() .asSingle(Response.self) .map { ResponseToModelMapperKt.toModel($0) } .catchError { throw handledKotlinException($0) } asSingle()の中⾝は?
  55. iOSのSwiftのコードから Kotlinのsuspend functionが 呼べない問題 extension Kotlinx_coroutines_core_nativeDeferred { func asSingle<ElementType>(_ elementType:

    ElementType.Type) -> Single<ElementType> { return Single<ElementType>.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になっている
  56. iOSのSwiftのコードから Kotlinのsuspend functionが 呼べない問題 extension Kotlinx_coroutines_core_nativeDeferred { func asSingle<ElementType>(_ elementType:

    ElementType.Type) -> Single<ElementType> { return Single<ElementType>.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になっている
  57. Kotlin Multiplatformと Dynamic feature module

  58. Dynamic feature module • 機能を利⽤される直前にダウンロードすることで、ダウンロード
 容量を削減できるAndroidの機能。 • STAR-ZEROさんという⽅から最⾼のPRがいただけました。
 https://github.com/DroidKaigi/conference-app-2019/pull/637 •

    (かなりシンプルな内容で、Dagger関連の事情をクリアしていた。)
  59. Kotlin Multiplatformと Dynamic feature moduleを 組み合わせるとリリースビルド出来ない

  60. Dynamic feature moduleで リリースビルド出来ない hogehoge.kotlin_moduleというクラスが、
 ビルド途中で重複してしまうために起こるようです。 現状、Workaroundがありません。 • https://github.com/DroidKaigi/conference-app-2019/issues/180

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

  62. APIの環境の切り替え

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

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

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

    actual fun apiEndpoint(): String = BuildConfig.API_ENDPOINT
  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”
  67. APIの環境の切り替え ⼀応環境の切り替えは実現できているがURLを⼆箇所に書いて いるので、うまく出来ていない せーいさんという⽅のこのライブラリを使えばうまくできるか も? https://github.com/yshrsmz/BuildKonfig

  68. ハマったポイント

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

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

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

  72. Android Studio上で Kotlin Multiplatform ModuleのクラスがUnresolved referenceになる 基本的には ・enableFeaturePreview(“GRADLE_METADATA") をsettings.gradleに記述 (これはGradle

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

    6.0からデフォルトになるらしい。) ・Gradleのバージョンを4.7にすることで解決。 GRADLE_METADATAとは?
  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では別々のアーキテクチャによって
 別のバイナリを使ったりできる
  75. Kotlin Multiplatform を使ってみてどうだったか?

  76. Kotlin Multiplatform を使ってみて • いろいろな問題はありましたが、
 普通にKotlinで書かれたクラスやメソッドなどが使えて共通化して
 いけたのは最⾼の体験でした。 • 普通にXCodeで補完も効いて
 キャストもうまく動いていい感じでした

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

    • Daggerとの組み合わせ • Kotlin Coroutinesとの組み合わせ • Dynamic feature moduleとの組み合わせ
  78. ありがとうございました