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

Kotlin Multiplatform for Android/iOS devs

Paolo Rotolo
December 03, 2022

Kotlin Multiplatform for Android/iOS devs

Paolo Rotolo

December 03, 2022
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 Vi rt ual

    Machine • Targets • iOS • Windows, Linux, Mac • Small devices • Interop with C and obj-C Kotlin/JVM • 100 % interop with Java
  3. KMM Roadmap 2017 Kotlin multiplatform Coroutines First class language for

    Android Support for iOS Alpha Beta 2018 2019 2020 2022 2022
  4. Pros & Cons ✅ Easy to use ✅ Low risks

    ✅ Easy integration with native code ✅ Shared codebase ✅ Active Community
  5. Pros & Cons ❌ Code generated not Swi ft -friendly

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

    getPlatform() fun greet(): String { return "Hello, ${platform.name}!" } }
  10. 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()
  11. 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()
  12. // 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. ./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") }
  14. Ktor suspend fun main() { val client = HttpClient() val

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

    install(ContentNegotiation) { json() } } val response: HttpResponse = client.get("https: / println(response.status) client.close() }
  16. install(Auth) { bearer { loadTokens { bearerTokenStorage.last() } refreshTokens {

    val refresh = this.oldTokens.refreshToken getNewToken(refresh) } } } suspend fun main() { val client = HttpClient() { install(ContentNegotiation) { json() } }
  17. 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") } }
  18. / 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) } } } }
  19. 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]) }
  20. Room Rx*, LiveData Timber Hilt, Koin SqlDelight / Realm Coroutines

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

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

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

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

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

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

    List<Todo> { val todos = getTodoFromServer() saveToDb(todos) return todos } }
  29. // 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() } }
  30. Coroutines @ToNativeClass(name = "TodoRepositoryIos") class TodoRepository { suspend fun fetchTodo():

    List<Todo> { val todos = getTodoFromServer() saveToDb(todos) return todos } }
  31. 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 })
  32. 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 >>
  33. @ToNativeClass(name = "TodoRepositoryIos") class TodoRepository { suspend fun fetchTodo(): List<Todo>

    { val todos = getTodoFromServer() saveToDb(todos) return todos } }
  34. @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() }
  35. @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>)
  36. @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 })
  37. @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!"))
  38. Flow class RandomIntGenerator { fun generateEach(interval: Long) = flow {

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

    while(true) { delay(interval) emit(Random.nextInt()) } } } lifecycleScope.launch { RandomIntGenerator().generateEach(ONE_SECOND).collect { print(it) } }
  40. 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) } }
  41. 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) } }
  42. 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) } }
  43. 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() } } } }
  44. 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() } } } }
  45. 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) }
  46. 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() }
  47. Sealed Class when (uiState) { is UIState.Data -> TODO() is

    UIState.Error - > TODO() is UIState.Loading - > TODO() }
  48. 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)