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

droidcon Italy 2022 - KMM Survival Guide: how to tackle everyday struggles between Kotlin and Swift

droidcon Italy 2022 - KMM Survival Guide: how to tackle everyday struggles between Kotlin and Swift

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

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 droidcon Italy 2022
  2. Working at Zest One in Chiasso, Switzerland Who I am?

    Emanuele Papa, Android Developer Find me at www.emanuelepapa.dev
  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.
  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?
  5. Everything looks cool! I can become an iOS developer! (or

    I can become an Android developer!) Expectation
  6. 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
  7. KMM directly supports Swift interoperability (KT-49521) Write shared code in

    a slightly different way to better support iOS development (and iOS developers) Solutions
  8. Sealed classes sealed class KMMIntResult data class SuccessKMMIntResult( val value:

    Int ) : KMMIntResult() data class ErrorKMMIntResult( val throwable: Throwable ) : KMMIntResult() Shared code
  9. 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
  10. 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
  11. 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
  12. Generic sealed class sealed class KMMResult<out Value> data class SuccessKMMResult<Value>(

    val value: Value ): KMMResult<Value>() data class ErrorKMMResult( val throwable: Throwable ): KMMResult<Nothing>() Shared code
  13. Generic sealed class fun getRandomIntWrappedInResult(): KMMResult<Int> { val isSuccess =

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

    randomIntText: String = when (randomInt) { is KMMResult.ErrorKMMResult -> { "Error: ${randomInt.throwable.message}" } is KMMResult.SuccessKMMResult -> { "Success: ${randomInt.value}" } }
  15. Generic sealed class iOS let randomInt: KMMResult<KotlinInt> = 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<KotlinInt>: randomIntText = "Success: \(success.value)" default: randomIntText = "This never happens" }
  16. Generics and sealed class First solution data class ErrorKMMResult( val

    throwable: Throwable ): KMMResult<Nothing>() data class ErrorKMMResult<Value>( val throwable: Throwable ): KMMResult<Value>() case let error as KMMResultErrorKMMResult<KotlinInt>: case let error as KMMResultErrorKMMResult:
  17. Generics and sealed class Second solution func toSwiftResult<Value>(kmmResult: KMMResult<Value>) ->

    SwiftResult<Value> { if let successResult = kmmResult as? KMMResultSuccessKMMResult<Value> { 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") }
  18. 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 and sealed classes Definitive solution
  19. public enum KMMResultKs<Value : AnyObject> { case successKMMResult(KMMResultSuccessKMMResult<Value>) case errorKMMResult(KMMResultErrorKMMResult<Value>)

    public var sealed: KMMResult<Value> { switch self { case .successKMMResult(let obj): return obj as shared.KMMResult<Value> case .errorKMMResult(let obj): return obj as shared.KMMResult<Value> } } public init(_ obj: KMMResult<Value>) { if let obj = obj as? shared.KMMResultSuccessKMMResult<Value> { self = .successKMMResult(obj) } else if let obj = obj as? shared.KMMResultErrorKMMResult<Value> { self = .errorKMMResult(obj) } else { fatalError("KMMResultKs not synchronized with KMMResult class") } } } Generics and sealed classes
  20. Inline class fun formatFirstAndLastName(firstName: String, lastName: String): String = "$firstName

    $lastName" formatFirstAndLastName("Emanuele", "Papa") formatFirstAndLastName("Papa", "Emanuele") Shared code
  21. 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
  22. 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
  23. 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
  24. Coroutines fun onShowMyNameClicked() { viewModelScope.launch { mutableState.value = mutableState.value.copy( formattedName

    = coroutinesProfileFormatter.formatFirstAndLastNameWithCoroutines( "John", "Doe" ) ) } }
  25. Coroutines func onShowMyNameClicked() { coroutinesProfileFormatter.formatFirstAndLastNameWithCoroutines( firstName: "John", lastName: "Doe" )

    { formattedName, error in DispatchQueue.main.async { self.state.formattedName = formattedName! } } }
  26. 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 } }
  27. Coroutines The new memory manager is Alpha. It's not production-ready

    and may be changed at any time 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. kotlin.native.binary.memoryModel=experimental https://github.com/JetBrains/kotlin/blob/master/kotlin-native/NEW_MM.md
  28. Result fun getRandomDouble(): Result<Double> { return if(Random.nextBoolean()) { Result.success(Random.nextDouble()) }

    else { Result.failure(Throwable("Can't generate a new double")) } } Shared code
  29. 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
  30. Result func onShowRandomDoubleClicked() { let randomDouble = RandomDoubleGeneratorKt.getRandomDouble() //randomDouble has

    type Any? state.formattedRandomDouble = String(describing: randomDouble) } iOS
  31. 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
  32. Pure Kotlin -> ✅ KMM library -> ✅ Custom (expect

    interface, actual 1 native library for Android, 1 native library for iOS) -> ✅❌ Third-party libraries
  33. 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
  34. 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
  35. 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
  36. Let's hope most of these issues will be officially fixed

    soon... but in the meanwhile, let's rock with KMM! The future
  37. Drop a line to [email protected] Get in touch with me

    www.emanuelepapa.dev We are hiring @ ZestOne