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 Virtual 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

Why we decided to give it a shot

Slide 10

Slide 10 text

Why we decided to give it a shot - team

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Roadmap 1. Validate the idea with a small library

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Roadmap 3. Make it work for iOS

Slide 15

Slide 15 text

Roadmap 4. Go on with multiplatform 5. Share as much code as we can

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Pros & Cons ❌ Code generated not Swift-friendly ❌ Build time can be slow on Xcode ❌ Few multiplatform libraries

Slide 18

Slide 18 text

Inside a KMM app

Slide 19

Slide 19 text

Tools Android Studio Kotlin Multiplatform Plugin Xcode / AppCode Kotlin Native Plugin

Slide 20

Slide 20 text

Tools Android Studio Kotlin Multiplatform 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

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

Slide 46

Slide 46 text

JetPack Multiplatform Libraries Compose DataStore Collections preview preview

Slide 47

Slide 47 text

The Dark side of KMM

Slide 48

Slide 48 text

Make it debuggable on iOS

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 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 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 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 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 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 67

Slide 67 text

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

Slide 68

Slide 68 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 69

Slide 69 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 70

Slide 70 text

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

Slide 71

Slide 71 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 72

Slide 72 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 73

Slide 73 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 74

Slide 74 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 75

Slide 75 text

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

Slide 76

Slide 76 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 77

Slide 77 text

Flow class RandomIntGenerator { fun generateEach(interval: Long) = flow { while(true) { delay(interval) emit(Random.nextInt()) } } } RandomIntGenerator() .generateEach(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 78

Slide 78 text

RandomIntGenerator() .generateEach(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 79

Slide 79 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 80

Slide 80 text

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

Slide 81

Slide 81 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 82

Slide 82 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 83

Slide 83 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 84

Slide 84 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 85

Slide 85 text

Exception Handling UNCHECKED EXCEPTIONS CHECKED EXCEPTIONS

Slide 86

Slide 86 text

Exception Handling

Slide 87

Slide 87 text

Exception Handling

Slide 88

Slide 88 text

Exception Handling

Slide 89

Slide 89 text

Error Handling

Slide 90

Slide 90 text

Exception Handling

Slide 91

Slide 91 text

Error Handling

Slide 92

Slide 92 text

Default parameters

Slide 93

Slide 93 text

Default parameters

Slide 94

Slide 94 text

Default parameters

Slide 95

Slide 95 text

Default parameters

Slide 96

Slide 96 text

KT-38685

Slide 97

Slide 97 text

Sealed Class

Slide 98

Slide 98 text

Sealed Class

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

Sealed Class

Slide 101

Slide 101 text

Sealed Class Enum

Slide 102

Slide 102 text

No content

Slide 103

Slide 103 text

Sealed Class

Slide 104

Slide 104 text

Context MyLibrary.getInstance( applicationContext)

Slide 105

Slide 105 text

Context let myLib = MyLibrary.getInstance() myLib.doSomething() val myLib = MyLibrary.getInstance( applicationContext) myLib.doSomething()

Slide 106

Slide 106 text

App Startup internal lateinit var applicationContext: Context private set public object MyLibrary class MyLibraryInitializer: Initializer { override fun create(context: Context): MyLibrary { applicationContext = context.applicationContext return MyLibrary } override fun dependencies(): List> > { return listOf() } }

Slide 107

Slide 107 text

Context let myLib = MyLibrary.getInstance() myLib.doSomething() val myLib = MyLibrary.getInstance( applicationContext) myLib.doSomething()

Slide 108

Slide 108 text

Context let myLib = MyLibrary.getInstance() myLib.doSomething() val myLib = MyLibrary.getInstance() myLib.doSomething()

Slide 109

Slide 109 text

Improved ObjC-Swift Interoperability 1.8.0

Slide 110

Slide 110 text

@ObjCName

Slide 111

Slide 111 text

@ObjCName userId: String){ ForUser ( fun deleteTodos TODO() }

Slide 112

Slide 112 text

@ObjCName func deleteTodos } ForUser( userId: String){ userId: String){ ForUser ( fun deleteTodos TODO() }

Slide 113

Slide 113 text

@ObjCName func deleteTodos } ( : String){ forUser fun deleteTodos TODO() } userId: String){ ForUser (

Slide 114

Slide 114 text

@ObjCName func deleteTodos } ( fun deleteTodos TODO() } userId: String){ ForUser ( @ObjCName(“deleteTodos”) @ObjCName(“forUser”) : String){ forUser

Slide 115

Slide 115 text

@HiddenFromObjC

Slide 116

Slide 116 text

@ShouldRefineInSwift

Slide 117

Slide 117 text

@ShouldRefineInSwift fun insertNewTodo( title: String, priority: Int ) ?

Slide 118

Slide 118 text

@ShouldRefineInSwift fun insertNewTodo( title: String, priority: Int ) repository.insertNewTodo( title: String, priority: ? ) KotlinInt? @ShouldRefineInSwift ⚠

Slide 119

Slide 119 text

@ShouldRefineInSwift ⚠ __ insertNewTodo(title: , priority: KotlinInt? ) String func insertNewTodo (title: String, priority: Int?){ }

Slide 120

Slide 120 text

@ShouldRefineInSwift __ func insertNewTodo (title: String, priority: Int?){ } insertNewTodo(title: , priority: ) title priority.toKotlinInt()

Slide 121

Slide 121 text

No content

Slide 122

Slide 122 text

What about the future

Slide 123

Slide 123 text

No content

Slide 124

Slide 124 text

Questions?

Slide 125

Slide 125 text

Thanks!