Slide 1

Slide 1 text

DroidKaigiアプリの Kotlin Multiplatform takahirom

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

DroidKaigi 公式アプリ

Slide 4

Slide 4 text

DroidKaigi 公式アプリ Android版 iOS版

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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 アノテーションを付ける

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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) } コードのイメージ

Slide 16

Slide 16 text

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のほうがいいです) コードのイメージ

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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つのセッションを表すクラス

Slide 23

Slide 23 text

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でも 利⽤できるようにする

Slide 24

Slide 24 text

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内

Slide 25

Slide 25 text

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で  使える⽇付や時間の  ライブラリを使う

Slide 26

Slide 26 text

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が⽇付などのライブラリを 開発予定らしい

Slide 27

Slide 27 text

開発初期に想定した構成

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

あとはissue⽴てて放置

Slide 30

Slide 30 text

kikuchyさんによる Kotlin Multiplatform Module化

Slide 31

Slide 31 text

問題が発⽣

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

DroidKaigiの構成 
 (Kotlin MPP特化版) "OESPJE.PEVMFT BQJ NPEFM Kotlin Multi Platform Project JPTDPNCJOFE J041SPKFDU kotlin.srcDirsで
 他のモジュールを
 参照する

Slide 35

Slide 35 text

この構成も問題が

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Kotlin Multiplatformと Dagger

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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)

Slide 46

Slide 46 text

Kotlin Multiplatformと Kotlin Coroutines

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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のコードをコールバックを使って ラップするメソッドを作成 成功時と失敗時のコールバック

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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を呼び出す

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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) }

Slide 54

Slide 54 text

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()の中⾝は?

Slide 55

Slide 55 text

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になっている

Slide 56

Slide 56 text

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になっている

Slide 57

Slide 57 text

Kotlin Multiplatformと Dynamic feature module

Slide 58

Slide 58 text

Dynamic feature module • 機能を利⽤される直前にダウンロードすることで、ダウンロード
 容量を削減できるAndroidの機能。 • STAR-ZEROさんという⽅から最⾼のPRがいただけました。
 https://github.com/DroidKaigi/conference-app-2019/pull/637 • (かなりシンプルな内容で、Dagger関連の事情をクリアしていた。)

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

APIの環境の切り替え

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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”

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

ハマったポイント

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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では別々のアーキテクチャによって
 別のバイナリを使ったりできる

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

ありがとうございました