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

Kotlin Multiplatform for Android/iOS devs v2

Kotlin Multiplatform for Android/iOS devs v2

Presented at DevFest Pisa - 01 April 2023

Paolo Rotolo

April 02, 2023
Tweet

More Decks by Paolo Rotolo

Other Decks in Technology

Transcript

  1. Kotlin Multiplatform for Android/iOS devs Paolo Rotolo Mobile developer @

    Nextome Anna Labellarte Mobile developer @ Nextome
  2. 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
  3. Pros & Cons ✅ Easy to use ✅ Low risks

    ✅ Easy integration with native code ✅ Shared codebase ✅ Active Community
  4. Pros & Cons ❌ Code generated not Swift-friendly ❌ Build

    time can be slow on Xcode ❌ Few multiplatform libraries
  5. 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 } }
  6. 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
  7. 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" } }
  8. package com.nextome.hellokmm class Greeting { private val platform: Platform =

    getPlatform() fun greet(): String { return "Hello, ${platform.name}!" } }
  9. 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()
  10. 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()
  11. // 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") } // [ ... ] }
  12. ./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") }
  13. Room Rx*, LiveData Timber Hilt, Koin SqlDelight / Realm Coroutines

    & Flow Kermit Koin ➡ ➡ ➡ ➡ Retrofit, OkHttp Gson, Moshi ➡ Ktor ➡ kotlinx-serialization
  14. Coroutines class TodoRepository { suspend fun fetchTodo(): List<Todo> { val

    todos = getTodoFromServer() saveToDb(todos) return todos } }
  15. Coroutines class TodoRepository { suspend fun fetchTodo(): List<Todo> { val

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

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

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

    todos = getTodoFromServer() saveToDb(todos) return todos } } scope.launch { val todo = TodoRepository().fetchTodo() } scope.cancel()
  19. 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") }
  20. Coroutines class TodoRepository { suspend fun fetchTodo(): List<Todo> { val

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

    List<Todo> { val todos = getTodoFromServer() saveToDb(todos) return todos } }
  22. // 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<List<Todo> = SuspendWrapper(scopeProvider, false) { wrapped.fetchTodo() } }
  23. Coroutines @ToNativeClass(name = "TodoRepositoryIos") class TodoRepository { suspend fun fetchTodo():

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

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

    List<Todo> { 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<List<Todo >>
  26. @ToNativeClass(name = "TodoRepositoryIos") class TodoRepository { suspend fun fetchTodo(): List<Todo>

    { val todos = getTodoFromServer() saveToDb(todos) return todos } }
  27. @ToNativeClass(name = "TodoRepositoryIos", launchOnScope = MainScopeProvider :: class) class TodoRepository

    { suspend fun fetchTodo(): List<Todo> { val todos = getTodoFromServer() saveToDb(todos) return todos } } @ExportedScopeProvider class MainScopeProvider : ScopeProvider { override val scope : CoroutineScope = MainScope() }
  28. @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<Todo>)
  29. @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 })
  30. @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!"))
  31. Flow class RandomIntGenerator { fun generateEach(interval: Long) = flow {

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

    while(true) { delay(interval) emit(Random.nextInt()) } } } lifecycleScope.launch { RandomIntGenerator().generateEach(ONE_SECOND).collect { print(it) } }
  33. 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) } }
  34. 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) } }
  35. RandomIntGenerator() .generateEach(interval: ONE_SECOND) .collect(collector: Collector<KotlinInt>() { value in print(value) })

    { (error) in print(error?.localizedDescription) } class Collector<T>: 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) } }
  36. class RandomIntGenerator { fun generateEach(interval: Long) = flow { while(true)

    { delay(interval) emit(Random.nextInt()) } } } fun <T> Flow<T>.wrap(): CFlow<T> = CFlow(this) class CFlow<T>(private val origin: Flow<T>) : Flow<T> 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() } } } }
  37. class RandomIntGenerator { fun generateEach(interval: Long): CFlow<Int> = flow {

    while(true) { delay(interval) emit(Random.nextInt()) } }.wrap() } fun <T> Flow<T>.wrap(): CFlow<T> = CFlow(this) class CFlow<T>(private val origin: Flow<T>) : Flow<T> 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() } } } }
  38. class RandomIntGenerator { fun generateEach(interval: Long): CFlow<Int> = flow {

    while(true) { delay(interval) emit(Random.nextInt()) } }.wrap() } RandomIntGenerator() .generateEach(interval: ONE_SECOND) .watch { (value: KotlinInt?) in print(value) }
  39. let disposable = RandomIntGenerator() .generateEach(interval: ONE_SECOND) .watch { (value: KotlinInt?)

    in print(value) } disposable.close() class RandomIntGenerator { fun generateEach(interval: Long): CFlow<Int> = flow { while(true) { delay(interval) emit(Random.nextInt()) } }.wrap() }
  40. Sealed Class when (uiState) { is UIState.Data -> TODO() is

    UIState.Error - > TODO() is UIState.Loading - > TODO() }
  41. App Startup internal lateinit var applicationContext: Context private set public

    object MyLibrary class MyLibraryInitializer: Initializer<MyLibrary> { override fun create(context: Context): MyLibrary { applicationContext = context.applicationContext return MyLibrary } override fun dependencies(): List<Class<out Initializer <*>> > { return listOf() } }
  42. @ObjCName func deleteTodos } ( fun deleteTodos TODO() } userId:

    String){ ForUser ( @ObjCName(“deleteTodos”) @ObjCName(“forUser”) : String){ forUser