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

Commit University November 2022 - KMM Survival Guide: how to tackle everyday struggles between Kotlin and Swift

Commit University November 2022 - KMM Survival Guide: how to tackle everyday struggles between Kotlin and Swift

Slides used during my talk at Commit University in November 2022 about how to use Kotlin Multiplatform Mobile, what issues are currently unsolved, and how to work around them!

Blog post: https://www.commitsoftware.it/kotlin-multiplatform-mobile-how-it-works/

Video: https://www.youtube.com/watch?v=SRP3nnAQ5P4

Emanuele Papa

November 20, 2022
Tweet

More Decks by Emanuele Papa

Other Decks in Programming

Transcript

  1. KMM Survival Guide:
    how to tackle everyday
    struggles between Kotlin
    and Swift
    Emanuele Papa
    Commit University November 2022

    View Slide

  2. Working at Zest One in Chiasso, Switzerland
    Who I am?
    Emanuele Papa, Android Developer
    Find me at www.emanuelepapa.dev

    View Slide

  3. What is KMM?
    Kotlin Multiplatform
    Mobile is an SDK for iOS
    and Android app
    development. It offers all
    the combined benefits of
    creating cross-platform
    and native apps.

    View Slide

  4. Shared module: you write Kotlin code as you are used to do.
    Android module: you see no difference and just use the shared module
    code
    iOS project: using Kotlin Native, the code is compiled into an
    AppleFramework like it was written in Objective-C.
    Then you can just use it in the same way as when you import an iOS
    dependency.
    How does it work?

    View Slide

  5. Swift and Kotlin are both modern languages and have a lot of
    syntax similarities
    Swift is like Kotlin

    View Slide

  6. Everything looks cool!


    I can become an iOS developer!


    (or I can become an Android developer!)
    Expectation

    View Slide

  7. Not everything on KMM works out of the box.
    Unfortunately, most of the issues arise because the
    Kotlin code is transformed into ObjectiveC code.
    Reality

    View Slide

  8. KMM directly supports Swift interoperability
    (KT-49521)
    Write shared code in a slightly different way to better
    support iOS development (and iOS developers)
    Solutions

    View Slide

  9. Primitive types
    fun getRandomInt(): Int = Random.nextInt(until = 10)
    Shared code

    View Slide

  10. Primitive types
    val randomInt: Int = getRandomInt()
    Android

    View Slide

  11. Primitive types
    let randomInt: Int32 = RandomNumberGeneratorKt.getRandomInt()
    iOS

    View Slide

  12. Sealed classes
    sealed class KMMIntResult
    data class SuccessKMMIntResult(
    val value: Int
    ) : KMMIntResult()
    data class ErrorKMMIntResult(
    val throwable: Throwable
    ) : KMMIntResult()
    Shared code

    View Slide

  13. Sealed classes
    fun getRandomIntWrappedInIntResult(): KMMIntResult {
    val isSuccess = Random.nextBoolean()
    return if(isSuccess) {
    SuccessKMMIntResult(Random.nextInt(until = 10))
    } else {
    ErrorKMMIntResult(RuntimeException("There was an error, Int not generated"))
    }
    }
    Shared code

    View Slide

  14. Sealed classes
    val randomInt: KMMIntResult = getRandomIntWrappedInIntResult()
    val randomIntText: String = when (randomInt) {
    is KMMIntResult.ErrorKMMIntResult -> {
    "Error: ${randomInt.throwable.message}"
    }
    is KMMIntResult.SuccessKMMIntResult -> {
    "Success: ${randomInt.value}"
    }
    }
    Android

    View Slide

  15. Sealed classes
    let randomInt: KMMIntResult = RandomNumberGeneratorKt.getRandomIntWrappedInIntResult()
    let randomIntText: String
    switch randomInt {
    case let error as KMMIntResult.ErrorKMMIntResult:
    randomIntText = "Error: \(error.throwable.message ?? error.throwable.description())"
    case let success as KMMIntResult.SuccessKMMIntResult:
    randomIntText = "Success: \(success.value)"
    default:
    randomIntText = "This never happens"
    }
    iOS

    View Slide

  16. Generic sealed class
    sealed class KMMResult
    data class SuccessKMMResult(
    val value: Value
    ): KMMResult()
    data class ErrorKMMResult(
    val throwable: Throwable
    ): KMMResult()
    Shared code

    View Slide

  17. Generic sealed class
    fun getRandomIntWrappedInResult(): KMMResult {
    val isSuccess = Random.nextBoolean()
    return if(isSuccess) {
    SuccessKMMResult(Random.nextInt(until = 10))
    } else {
    ErrorKMMResult(RuntimeException("There was an error, Int not generated"))
    }
    }
    Shared code

    View Slide

  18. Generic sealed class
    Android
    val randomInt: KMMResult = getRandomIntWrappedInResult()
    val randomIntText: String = when (randomInt) {
    is KMMResult.ErrorKMMResult -> {
    "Error: ${randomInt.throwable.message}"
    }
    is KMMResult.SuccessKMMResult -> {
    "Success: ${randomInt.value}"
    }
    }

    View Slide

  19. Generic sealed class
    iOS
    let randomInt: KMMResult = RandomNumberGeneratorKt.getRandomIntWrappedInIntResult()
    let randomIntText: String
    switch randomInt {
    case let error as KMMResultErrorKMMResult:
    randomIntText = "Error: \(error.throwable.message ?? error.throwable.description())"
    case let success as KMMResultSuccessKMMResult:
    randomIntText = "Success: \(success.value)"
    default:
    randomIntText = "This never happens"
    }

    View Slide

  20. Generics sealed class
    First solution
    data class ErrorKMMResult(
    val throwable: Throwable
    ): KMMResult()
    data class ErrorKMMResult(
    val throwable: Throwable
    ): KMMResult()
    case let error as
    KMMResultErrorKMMResult:
    case let error as
    KMMResultErrorKMMResult:

    View Slide

  21. Generics sealed class
    Second solution
    enum SwiftResult {
    case error(String)
    case success(Value)
    }

    View Slide

  22. Generics sealed class
    Second solution
    func toSwiftResult(kmmResult: KMMResult) -> SwiftResult {
    if let successResult = kmmResult as? KMMResultSuccessKMMResult {
    return SwiftResult.success(successResult.value!)
    }
    if let errorResult = kmmResult as? KMMResultErrorKMMResult {
    return SwiftResult.error(errorResult.throwable.message ?? errorResult.throwable.description())
    }
    return SwiftResult.error("Unexpected error converting to SwiftResult")
    }

    View Slide

  23. Use moko-kswift
    https://github.com/icerockdev/moko-kswift
    KSwift is gradle plugin to generate Swift-friendly API for Kotlin/Native framework.
    Note: at the moment, all subclasses in a sealed class must be nested, otherwise the
    generation fails.
    Generics sealed classes
    Definitive solution

    View Slide

  24. public enum KMMResultKs {
    case successKMMResult(KMMResultSuccessKMMResult)
    case errorKMMResult(KMMResultErrorKMMResult)
    public var sealed: KMMResult {
    switch self {
    case .successKMMResult(let obj):
    return obj as shared.KMMResult
    case .errorKMMResult(let obj):
    return obj as shared.KMMResult
    }
    }
    public init(_ obj: KMMResult) {
    if let obj = obj as? shared.KMMResultSuccessKMMResult {
    self = .successKMMResult(obj)
    } else if let obj = obj as? shared.KMMResultErrorKMMResult {
    self = .errorKMMResult(obj)
    } else {
    fatalError("KMMResultKs not synchronized with KMMResult class")
    }
    }
    }
    Generics sealed classes

    View Slide

  25. Inline class
    fun formatFirstAndLastName(firstName: String, lastName: String): String =
    "$firstName $lastName"
    formatFirstAndLastName("Emanuele", "Papa")
    formatFirstAndLastName("Papa", "Emanuele")
    Shared code

    View Slide

  26. Inline class
    fun formatFirstAndLastName(firstName: FirstName, lastName: LastName): String =
    "${firstName.firstName} ${lastName.lastName}"
    value class FirstName(val firstName: String)
    value class LastName(val lastName: String)
    formatFirstAndLastName(
    FirstName("John"),
    LastName("Doe")
    )
    Shared code

    View Slide

  27. Inline class
    ProfileFormatterKt.formatFirstAndLastName(firstName: , lastName: )
    KT-32352
    iOS

    View Slide

  28. Inline class
    fun formatFirstAndLastName(
    firstName: FirstNameIos,
    lastName: LastNameIos
    ): String {
    return formatFirstAndLastName(FirstName(firstName.firstName), LastName(lastName.lastName))
    }
    data class FirstNameIos(
    val firstName: String
    )
    data class LastNameIos(
    val lastName: String
    )
    Shared iOS code

    View Slide

  29. Inline class
    ProfileFormatterKt.formatFirstAndLastName(
    firstName: FirstNameIos(firstName: "John"),
    lastName: LastNameIos(lastName: "Doe")
    )
    iOS code

    View Slide

  30. A coroutine is a concurrency design pattern that you
    can use on Android to simplify code that executes
    asynchronously.
    All of this doesn't exist in ObjectiveC
    Coroutines

    View Slide

  31. Coroutines
    class CoroutinesProfileFormatter {
    suspend fun formatFirstAndLastNameWithCoroutines(
    firstName: String,
    lastName: String
    ): String {
    delay(1000)
    return "$firstName $lastName"
    }
    }
    Shared code

    View Slide

  32. Coroutines
    fun onShowMyNameClicked() {
    viewModelScope.launch {
    mutableState.value = mutableState.value.copy(
    formattedName = coroutinesProfileFormatter.formatFirstAndLastNameWithCoroutines(
    "John",
    "Doe"
    )
    )
    }
    }

    View Slide

  33. Coroutines
    func onShowMyNameClicked() {
    coroutinesProfileFormatter.formatFirstAndLastNameWithCoroutines(
    firstName: "John",
    lastName: "Doe"
    ) { formattedName, error in
    DispatchQueue.main.async {
    self.state.formattedName = formattedName!
    }
    }
    }

    View Slide

  34. Coroutines
    https://github.com/rickclephas/KMP-NativeCoroutines
    Solution
    func onShowMyNameWithCombineClicked() {
    let formatFirstAndLastNamePublisher = createPublisher(
    for: coroutinesProfileFormatter.formatFirstAndLastNameWithCoroutinesNative(
    firstName: "Async",
    lastName: "Doe"
    ))
    formatFirstAndLastNamePublisher
    .subscribe(on: DispatchQueue.main)
    .receive(on: DispatchQueue.main)
    .sink { completion in
    print("Received completion: \(completion)")
    } receiveValue: { value in
    self.state.formattedNameWithAsync = value
    }
    }

    View Slide

  35. Coroutines
    The new memory manager is just being promoted to Beta and it's enabled by
    default from Kotlin 1.7.20.
    In the new memory manager (MM), we're lifting restrictions on object sharing:
    there's no need to freeze objects to share them between threads anymore.
    To enable it in Kotlin < 1.7.20
    kotlin.native.binary.memoryModel=experimental
    https://github.com/JetBrains/kotlin/blob/master/kotlin-native/NEW_MM.md

    View Slide

  36. Result
    fun getRandomDouble(): Result {
    return if(Random.nextBoolean()) {
    Result.success(Random.nextDouble())
    } else {
    Result.failure(Throwable("Can't generate a new double"))
    }
    }
    Shared code

    View Slide

  37. Result
    fun onShowRandomDoubleClicked() {
    val randomDouble = getRandomDouble()
    randomDouble.fold(
    onSuccess = { double ->
    mutableState.value = mutableState.value.copy(formattedRandomDouble = double.toString())
    },
    onFailure = { throwable ->
    mutableState.value = mutableState.value.copy(formattedRandomDouble = throwable.toString())
    })
    }
    Android

    View Slide

  38. Result
    func onShowRandomDoubleClicked() {
    let randomDouble = RandomDoubleGeneratorKt.getRandomDouble()
    //randomDouble has type Any?
    state.formattedRandomDouble = String(describing: randomDouble)
    }
    iOS

    View Slide

  39. This is another thing which code generation might solve...
    Default parameters
    KT-38685

    View Slide

  40. Exception handling
    Kotlin -> Only unchecked exceptions
    Swift -> Only checked errors
    Exception specified with @Throws -> Propagated as NSError
    Exception without @Throws -> iOS crash
    Always catch Exceptions in the shared code and return a Result like class

    View Slide

  41. Pure Kotlin -> ✅
    KMM library -> ✅
    Custom (expect interface, actual 1 native library for Android, 1 native
    library for iOS) -> ✅❌
    Third-party libraries

    View Slide

  42. class
    SystemInfoRetrieverImpl(
    private val systemInfoRetriever: SystemInfoRetrieverNativeWrapper
    ): SystemInfoRetriever
    interface
    SystemInfoRetrieverNativeWrapper {
    fun getSystemName(): String
    }
    class
    SystemInfoRetrieverAndroid :
    SystemInfoRetrieverNativeWrapper
    class
    SystemInfoRetrieverIOS:
    SystemInfoRetrieverNativeWrapper
    interface
    SystemInfoRetriever {
    fun getSystemName(): String
    }
    Third-party libraries

    View Slide

  43. Use this Gradle plugin to create a Swift Package Manager manifest
    and an XCFramework for iOS devs when you create a KMM library
    SPM
    https://github.com/ge-org/multiplatform-swiftpackage

    View Slide

  44. iOS devs will like:
    an Xcode plugin which allows debugging of Kotlin code
    running in an iOS application, directly from Xcode.
    https://github.com/touchlab/xcode-kotlin
    IDEs

    View Slide

  45. Android devs will like:
    an IDE similar to IntelliJ IDEA/Android Studio but for
    iOS: AppCode!
    IDEs

    View Slide

  46. Let's hope most of these issues will be
    officially fixed soon...
    but in the meanwhile, let's rock with
    KMM!
    The future

    View Slide

  47. Thank you!

    View Slide

  48. Drop a line to [email protected]
    Get in touch with me
    www.emanuelepapa.dev

    View Slide