Kotlin Multiplatform Mobile (KMM) è un SDK per lo sviluppo di applicazioni Android ed iOS che consente agli sviluppatori di condividere la business logic mantenendo UI/UX native.
Ogni SDK/framework cross/multi platform ha i suoi pro ed i suoi contro, e purtroppo KMM non è l'eccezione che conferma la regola.
Se sei uno sviluppatore Android potresti pensare che tutto funzionerà correttamente, ma purtroppo non sarà così quando dovrai confrontarti con Swift.
Se sei uno sviluppatore iOS saprai che Swift è simile a Kotlin, ma non in tutto, dovrai quindi conoscere alcune sue caratteristiche.
In questo talk vedremo quali sono i problemi che si possono riscontrare nell'interoperabilità tra Kotlin e Swift, i motivi che li causano, e come risolverli.
KMM survival guide: how to tackle struggles between Kotlin and Swift
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
12. Sealed classes
sealed class KMMIntResult
data class SuccessKMMIntResult(
val value: Int
) : KMMIntResult()
data class ErrorKMMIntResult(
val throwable: Throwable
) : KMMIntResult()
Shared code
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
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
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
16. 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
17. 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
18. 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}"
}
}
19. 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"
}
20. 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:
22. 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")
}
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
24. 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
25. Inline class
fun formatFirstAndLastName(firstName: String, lastName: String): String =
"$firstName $lastName"
formatFirstAndLastName("Emanuele", "Papa")
formatFirstAndLastName("Papa", "Emanuele")
Shared code
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
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
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
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
}
}
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
36. 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
38. Result
func onShowRandomDoubleClicked() {
let randomDouble = RandomDoubleGeneratorKt.getRandomDouble()
//randomDouble has type Any?
state.formattedRandomDouble = String(describing: randomDouble)
}
iOS
39. This is another thing which code generation might solve...
Default parameters
KT-38685
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
41. Pure Kotlin -> ✅
KMM library -> ✅
Custom (expect interface, actual 1 native library for Android, 1 native
library for iOS) -> ✅❌
Third-party libraries
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
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
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
45. Android devs will like:
an IDE similar to IntelliJ IDEA/Android Studio but for
iOS: AppCode!
IDEs
46. Let's hope most of these issues will be
officially fixed soon...
but in the meanwhile, let's rock with
KMM!
The future