Slide 1

Slide 1 text

Kotlin Multiplatform for Android/iOS devs Paolo Rotolo Mobile developer @ Nextome Anna Labellarte Mobile developer @ Nextome

Slide 2

Slide 2 text

What is Kotlin Multiplaform

Slide 3

Slide 3 text

What is Kotlin Multiplatform

Slide 4

Slide 4 text

What is Kotlin Multiplatform

Slide 5

Slide 5 text

What is Kotlin Multiplatform Kotlin/Native • No Vi rt ual Machine • Targets • iOS • Windows, Linux, Mac • Small devices • Interop with C and obj-C Kotlin/JVM • 100 % interop with Java

Slide 6

Slide 6 text

Kotlin Multiplatform Mobile Image taken from: https://dev.to/anioutkajarkova/kotlin-multiplatform-practical-multithreading-part-1-4357 Kotlin/Native Kotlin/JVM BETA

Slide 7

Slide 7 text

Kotlin Multiplatform Mobile (KMM) BETA

Slide 8

Slide 8 text

Kotlin Multiplatform Mobile (KMM) BETA

Slide 9

Slide 9 text

KMM Roadmap 2017 Kotlin multiplatform Coroutines First class language for Android Support for iOS Alpha Beta 2018 2019 2020 2022 2022

Slide 10

Slide 10 text

Why we decided to give it a shot

Slide 11

Slide 11 text

Why we decided to give it a shot - team

Slide 12

Slide 12 text

Why we decided to give it a shot - SDK product workflow

Slide 13

Slide 13 text

Roadmap 1. Validate the idea with a small library

Slide 14

Slide 14 text

Roadmap 2. Transform the existing android project Room Retro fi t Resources DI DateTime

Slide 15

Slide 15 text

Roadmap 3. Make it work for iOS

Slide 16

Slide 16 text

Roadmap 4. Go on with multipla tf orm 5. Share as much code as we can

Slide 17

Slide 17 text

Pros & Cons ✅ Easy to use ✅ Low risks ✅ Easy integration with native code ✅ Shared codebase ✅ Active Community

Slide 18

Slide 18 text

Pros & Cons ❌ Code generated not Swi ft -friendly ❌ Build time can be slow on Xcode ❌ Few multipla tf orm libraries

Slide 19

Slide 19 text

Inside a KMM app

Slide 20

Slide 20 text

Tools Android Studio Kotlin Multipla tf orm Plugin Xcode / AppCode Kotlin Native Plugin

Slide 21

Slide 21 text

How to start

Slide 22

Slide 22 text

Root project

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

plugins { kotlin("multiplatform") kotlin("native.cocoapods") id("com.android.library") } kotlin { android() ios() }

Slide 26

Slide 26 text

kotlin { android() ios() sourceSets { val commonMain by getting { dependencies { implementation( "io.ktor:ktor-client-core:$ktor_version") } } val androidMain by getting { dependencies { implementation( "androidx.work:work-runtime-ktx:$work_version") } } val iosMain by getting } }

Slide 27

Slide 27 text

val commonMain by getting { dependencies { implementation( "io.ktor:ktor-client-core:$ktor_version") } } val androidMain by getting { dependencies { implementation( "androidx.work:work-runtime-ktx:$work_version") } } val iosMain by getting } } val commonTest by getting val androidTest by getting val iosTest by getting

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

plugins { kotlin("multiplatform") kotlin("native.cocoapods") id("com.android.library") } kotlin { android() ios() cocoapods { pod("SSZipArchive") } sourceSets { val commonMain by getting { dependencies { implementation( "io.ktor:ktor-client-core:$ktor_version" } }

Slide 31

Slide 31 text

How to write multiplatform code

Slide 32

Slide 32 text

How to write multiplatform code

Slide 33

Slide 33 text

package com.nextome.hellokmm class Greeting { private val platform: Platform = getPlatform() fun greet(): String { return "Hello, ${platform.name}!" } }

Slide 34

Slide 34 text

package com.nextome.hellokmm interface Platform { val name: String } expect fun getPlatform(): Platform

Slide 35

Slide 35 text

package com.nextome.hellokmm import android.os.Build.VERSION class AndroidPlatform : Platform { override val name: String = "Android ${VERSION.SDK_INT}" } actual fun getPlatform(): Platform = AndroidPlatform()

Slide 36

Slide 36 text

package com.nextome.hellokmm import platform.UIKit.UIDevice class IOSPlatform: Platform { override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion } actual fun getPlatform(): Platform = IOSPlatform()

Slide 37

Slide 37 text

Publish multiplatform libraries

Slide 38

Slide 38 text

/ / build.gradle.kts (:shared) plugins { kotlin("multiplatform") id(“com.android.library”) }

Slide 39

Slide 39 text

/ / build.gradle.kts (:shared) plugins { kotlin("multiplatform") id(“com.android.library”) id("maven-publish") } group = "com.nextome.hellokmm" version = "1.0.0"

Slide 40

Slide 40 text

// build.gradle.kts (:shared) plugins { kotlin("multiplatform") id(“com.android.library”) id("maven-publish") } group = "com.nextome.hellokmm" version = “1.0.0" kotlin { android{ publishLibraryVariants("release", "debug") } // [ ... ] }

Slide 41

Slide 41 text

./gradlew publishToMavenLocal // build.gradle.kts (:shared) plugins { kotlin("multiplatform") id(“com.android.library”) id("maven-publish") } group = "com.nextome.hellokmm" version = “1.0.0" kotlin { android{ publishLibraryVariants("release", "debug") }

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

sourceSets { val commonMain by getting { dependencies { implementation("com.nextome.hellokmm:hellokmm:1.0.0") } } } Consume libraries

Slide 44

Slide 44 text

Useful libraries for KMM

Slide 45

Slide 45 text

Retrofit, OkHttp ➡ Ktor

Slide 46

Slide 46 text

Ktor suspend fun main() { val client = HttpClient() val response: HttpResponse = client.get("https: / / ktor.io/") println(response.status) client.close() }

Slide 47

Slide 47 text

Ktor suspend fun main() { val client = HttpClient() { install(ContentNegotiation) { json() } } val response: HttpResponse = client.get("https: / println(response.status) client.close() }

Slide 48

Slide 48 text

install(Auth) { bearer { loadTokens { bearerTokenStorage.last() } refreshTokens { val refresh = this.oldTokens.refreshToken getNewToken(refresh) } } } suspend fun main() { val client = HttpClient() { install(ContentNegotiation) { json() } }

Slide 49

Slide 49 text

bearerTokenStorage.last() } refreshTokens { val refresh = this.oldTokens.refreshToken getNewToken(refresh) } } } } / install(Logging) { logger = Logger.DEFAULT level = LogLevel.HEADERS filter { request -> request.url.host.contains("ktor.io") } }

Slide 50

Slide 50 text

/ filter { request -> request.url.host.contains("ktor.io") } } HttpResponseValidator { handleResponseExceptionWithRequest { exception, request -> val clientException = exception as? ClientRequestException ?: return@handleResponseExceptionWithRequest when (clientException.response.status) { HttpStatusCode.NotFound - > { throw MissingPageException(exceptionResponse) } } } }

Slide 51

Slide 51 text

Gson, Moshi ➡ kotlinx-serialization

Slide 52

Slide 52 text

kotlinx-serialization @Serializable data class User(val name: String, val email: String) fun main() { val data = User("Paolo", “[email protected]”) val serializedData: String = Json.encodeToString(data) println(serializedData) // {“name”:"Paolo","email":"[email protected]"} val deserializedData: User = Json.decodeFromString(serializedData) println(deserializedData) // Project(name=Paolo, [email protected]) }

Slide 53

Slide 53 text

Room Rx*, LiveData Timber Hilt, Koin SqlDelight / Realm Coroutines & Flow Kermit Koin ➡ ➡ ➡ ➡ Retrofit, OkHttp Gson, Moshi ➡ Ktor ➡ kotlinx-serialization

Slide 54

Slide 54 text

SharedPreferences, DataStore -> DataStore

Slide 55

Slide 55 text

JetPack Multiplatform Libraries Compose DataStore Collections preview preview

Slide 56

Slide 56 text

The Dark side of KMM

Slide 57

Slide 57 text

Make it debuggable on iOS

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

Data Types fun insertNewTodo( title: String, priority: Int )

Slide 64

Slide 64 text

Data Types fun insertNewTodo( title: String, priority: Int ) repository.insertNewTodo( title: String, priority: ? Int32 )

Slide 65

Slide 65 text

Data Types fun insertNewTodo( title: String, priority: Int ) repository.insertNewTodo( title: String, priority: ? ) KotlinInt?

Slide 66

Slide 66 text

Coroutines class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } }

Slide 67

Slide 67 text

Coroutines class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } } scope.launch { val todo = TodoRepository().fetchTodo() }

Slide 68

Slide 68 text

Coroutines class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } } TodoRepository().fetchTodo {todos, error in } func loadTodo() async throws { let todo = try await TodoRepository().fetchTodo() }

Slide 69

Slide 69 text

Coroutines class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } } scope.launch { val todo = TodoRepository().fetchTodo() }

Slide 70

Slide 70 text

Coroutines class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } } scope.launch { val todo = TodoRepository().fetchTodo() } scope.cancel()

Slide 71

Slide 71 text

Koru Inspired by https://touchlab.co/kotlin-coroutines-rxswift/

Slide 72

Slide 72 text

Koru Inspired by https://touchlab.co/kotlin-coroutines-rxswift/ plugins { // add ksp and koru compiler plugin id("com.google.devtools.ksp") version "1.6.21-1.0.6" id("com.futuremind.koru").version("0.11.1") } kotlin { sourceSets { val commonMain by getting { dependencies { // add library dependency implementation("com.futuremind:koru:0.11.1") } } val iosMain by creating { ... } } } koru { nativeSourceSetNames = listOf("iosMain") }

Slide 73

Slide 73 text

Coroutines class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } }

Slide 74

Slide 74 text

Coroutines @ToNativeClass(name = "TodoRepositoryIos") class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } }

Slide 75

Slide 75 text

// build/generated/ksp/TodoRepositoryIos.kt public class TodoRepositoryIos( private val wrapped: TodoRepository, private val scopeProvider: ScopeProvider?, ) { public constructor(wrapped: TodoRepository) : this(wrapped,exportedScopeProvider_mainScopeProvider) public fun fetchTodo(): SuspendWrapper = SuspendWrapper(scopeProvider, false) { wrapped.fetchTodo() } }

Slide 76

Slide 76 text

Coroutines @ToNativeClass(name = "TodoRepositoryIos") class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } }

Slide 77

Slide 77 text

Coroutines @ToNativeClass(name = "TodoRepositoryIos") class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } } let repo = TodoRepositoryIos( wrapped: TodoRepository(), scope: coroutineScope) repo.getTodoWrapped().subscribe( onSuccess: { (array: NSArray?) -> () in }, onThrow: { throwable in })

Slide 78

Slide 78 text

Coroutines @ToNativeClass(name = "TodoRepositoryIos") class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } } let repo = TodoRepositoryIos( wrapped: TodoRepository(), scope: coroutineScope) repo.getTodoWrapped().subscribe( onSuccess: { (array: NSArray?) -> () in }, onThrow: { throwable in }) SuspendWrapper>

Slide 79

Slide 79 text

@ToNativeClass(name = "TodoRepositoryIos") class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } }

Slide 80

Slide 80 text

@ToNativeClass(name = "TodoRepositoryIos", launchOnScope = MainScopeProvider :: class) class TodoRepository { suspend fun fetchTodo(): List { val todos = getTodoFromServer() saveToDb(todos) return todos } } @ExportedScopeProvider class MainScopeProvider : ScopeProvider { override val scope : CoroutineScope = MainScope() }

Slide 81

Slide 81 text

@ToNativeClass(name = "TodoRepositoryIos", launchOnScope = MainScopeProvider :: class) class TodoRepository { suspend fun fetchTodoList(): TodoList { val todos = getTodoFromServer() saveToDb(todos) return TodoList(todos) } } @ExportedScopeProvider class MainScopeProvider : ScopeProvider { override val scope : CoroutineScope = MainScope() } data class TodoList(val list: List)

Slide 82

Slide 82 text

@ToNativeClass(name = "TodoRepositoryIos", launchOnScope = MainScopeProvider :: class) class TodoRepository { suspend fun fetchTodoList(): TodoList { val todos = getTodoFromServer() saveToDb(todos) return TodoList(todos) } } Coroutines let repo = TodoRepositoryIos(wrapped: TodoRepository()) repo.fetchTodoList().subscribe(onSuccess: { (list: TodoList?) in }, onThrow: { (throwable: KotlinThrowable) in })

Slide 83

Slide 83 text

@ToNativeClass(name = "TodoRepositoryIos", launchOnScope = MainScopeProvider :: class) class TodoRepository { suspend fun fetchTodoList(): TodoList { val todos = getTodoFromServer() saveToDb(todos) return TodoList(todos) } } Coroutines let repo = TodoRepositoryIos(wrapped: TodoRepository()) let job = repo.fetchTodoList().subscribe(onSuccess: { (list: TodoList?) in }, onThrow: { (throwable: KotlinThrowable) in }) job.cancel(cause: KotlinCancellationException(message: "Stop it!"))

Slide 84

Slide 84 text

Flow class RandomIntGenerator { fun generateEach(interval: Long) = flow { while(true) { delay(interval) emit(Random.nextInt()) } } }

Slide 85

Slide 85 text

Flow class RandomIntGenerator { fun generateEach(interval: Long) = flow { while(true) { delay(interval) emit(Random.nextInt()) } } } lifecycleScope.launch { RandomIntGenerator().generateEach(ONE_SECOND).collect { print(it) } }

Slide 86

Slide 86 text

Flow class RandomIntGenerator { fun generateEach(interval: Long) = flow { while(true) { delay(interval) emit(Random.nextInt()) } } } RandomIntGenerator() .generateEachTest(interval: Int64(1000)) .collect(collector: Collector()) { error in } class Collector: Kotlinx_coroutines_coreFlowCollector{ func emit(value: Any?, completionHandler: @escaping (Error?) -> Void) { print(value) completionHandler(nil) } }

Slide 87

Slide 87 text

RandomIntGenerator() .generateEachTest(interval: Int64(1000)) .collect(collector: Collector()) { error in } class Collector: Kotlinx_coroutines_coreFlowCollector{ func emit(value: Any?, completionHandler: @escaping (Error?) -> Void) { print(value) completionHandler(nil) } }

Slide 88

Slide 88 text

RandomIntGenerator() .generateEach(interval: ONE_SECOND) .collect(collector: Collector() { value in print(value) }) { (error) in print(error?.localizedDescription) } class Collector: Kotlinx_coroutines_coreFlowCollector { let callback:(T) -> Void init(callback: @escaping (T) -> Void) { self.callback = callback } func emit(value: Any?, completionHandler: @escaping (Error?) -> Void) { callback(value as! T) completionHandler(nil) } }

Slide 89

Slide 89 text

class RandomIntGenerator { fun generateEach(interval: Long) = flow { while(true) { delay(interval) emit(Random.nextInt()) } } }

Slide 90

Slide 90 text

class RandomIntGenerator { fun generateEach(interval: Long) = flow { while(true) { delay(interval) emit(Random.nextInt()) } } } fun Flow.wrap(): CFlow = CFlow(this) class CFlow(private val origin: Flow) : Flow by origin { fun watch(block: (T) -> Unit): Closeable { val job = Job() onEach { block(it) }.launchIn(CoroutineScope(Dispatchers.Main + job)) return object : Closeable { override fun close() { job.cancel() } } } }

Slide 91

Slide 91 text

class RandomIntGenerator { fun generateEach(interval: Long): CFlow = flow { while(true) { delay(interval) emit(Random.nextInt()) } }.wrap() } fun Flow.wrap(): CFlow = CFlow(this) class CFlow(private val origin: Flow) : Flow by origin { fun watch(block: (T) > val job = Job() onEach { block(it) }.launchIn(CoroutineScope(Dispatchers.Main + job)) return object : Closeable { override fun close() { job.cancel() } } } }

Slide 92

Slide 92 text

class RandomIntGenerator { fun generateEach(interval: Long): CFlow = flow { while(true) { delay(interval) emit(Random.nextInt()) } }.wrap() } RandomIntGenerator() .generateEach(interval: ONE_SECOND) .watch { (value: KotlinInt?) in print(value) }

Slide 93

Slide 93 text

let disposable = RandomIntGenerator() .generateEach(interval: ONE_SECOND) .watch { (value: KotlinInt?) in print(value) } disposable.close() class RandomIntGenerator { fun generateEach(interval: Long): CFlow = flow { while(true) { delay(interval) emit(Random.nextInt()) } }.wrap() }

Slide 94

Slide 94 text

Error Handling UNCHECKED EXCEPTIONS CHECKED EXCEPTIONS

Slide 95

Slide 95 text

Error Handling

Slide 96

Slide 96 text

Error Handling

Slide 97

Slide 97 text

Error Handling

Slide 98

Slide 98 text

Error Handling

Slide 99

Slide 99 text

Error Handling

Slide 100

Slide 100 text

Error Handling

Slide 101

Slide 101 text

Error Handling

Slide 102

Slide 102 text

Default parameters

Slide 103

Slide 103 text

Default parameters

Slide 104

Slide 104 text

Default parameters

Slide 105

Slide 105 text

Default parameters

Slide 106

Slide 106 text

Sealed Class

Slide 107

Slide 107 text

Sealed Class

Slide 108

Slide 108 text

Sealed Class when (uiState) { is UIState.Data -> TODO() is UIState.Error - > TODO() is UIState.Loading - > TODO() }

Slide 109

Slide 109 text

Sealed Class

Slide 110

Slide 110 text

Sealed Class Enum

Slide 111

Slide 111 text

No content

Slide 112

Slide 112 text

Sealed Class

Slide 113

Slide 113 text

In the end… Share common logic in Kotlin Work together to make it work be tt er!

Slide 114

Slide 114 text

In the end… Share common logic in Kotlin Work together to make it work be tt er! Sometimes it could be ugly but we’re ge tt ing there (fast)

Slide 115

Slide 115 text

Thanks!