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
  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. Swift and Kotlin are both modern languages and have a

    lot of syntax similarities Swift is like Kotlin
  6. Everything looks cool! I can become an iOS developer! (or

    I can become an Android developer!) Expectation
  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
  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
  9. Sealed classes sealed class KMMIntResult data class SuccessKMMIntResult( val value:

    Int ) : KMMIntResult() data class ErrorKMMIntResult( val throwable: Throwable ) : KMMIntResult() Shared code
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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}" } }
  16. 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" }
  17. Generics 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:
  18. Generics 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") }
  19. 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
  20. 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 sealed classes
  21. Inline class fun formatFirstAndLastName(firstName: String, lastName: String): String = "$firstName

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

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

    { formattedName, error in DispatchQueue.main.async { self.state.formattedName = formattedName! } } }
  27. 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 } }
  28. 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
  29. 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
  30. 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
  31. Result func onShowRandomDoubleClicked() { let randomDouble = RandomDoubleGeneratorKt.getRandomDouble() //randomDouble has

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

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

    soon... but in the meanwhile, let's rock with KMM! The future