KMP-NativeCoroutines
A library to use Kotlin Coroutines from Swift code in KMP apps.
Why this library?
Both KMP and Kotlin Coroutines are amazing, but together they have some limitations.
The most important limitation is cancellation support.
Kotlin suspend functions are exposed to Swift as functions with a completion handler.
This allows you to easily use them from your Swift code, but it doesn't support cancellation.
[!NOTE]
While Swift 5.5 brings async functions to Swift, it doesn't solve this issue.
For interoperability with ObjC all functions with a completion handler can be called like an async function.
This means starting with Swift 5.5 your Kotlin suspend functions will look like Swift async functions.
But that's just syntactic sugar, so there's still no cancellation support.
Besides cancellation support, ObjC doesn't support generics on protocols.
So all the Flow interfaces lose their generic value type which make them hard to use.
This library solves both of these limitations 😄.
[!NOTE]
Looking to try Swift export?
Read about its current state and limitations in SWIFT_EXPORT.md.
Compatibility
The latest version of the library uses Kotlin version 2.4.0.
Compatibility versions for older and/or preview Kotlin versions are also available:
You can choose from a couple of Swift implementations.
Depending on the implementation you can support as low as iOS 9, macOS 10.9, tvOS 9 and watchOS 3:
Installation
The library consists of a Kotlin and Swift part which you'll need to add to your project.
The Kotlin part is available on Maven Central and the Swift part can be installed via CocoaPods
or the Swift Package Manager.
Make sure to always use the same versions for all the libraries!
Kotlin
For Kotlin just add the plugin to your build.gradle.kts:
plugins {
id("com.rickclephas.kmp.nativecoroutines") version "1.0.4"
}
and make sure to opt in to the experimental @ObjCName annotation:
kotlin.sourceSets.all {
languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")
}
Swift (Swift Package Manager)
The Swift implementations are available via the Swift Package Manager.
Just add it to your Package.swift file:
dependencies: [
.package(url: "https://github.com/rickclephas/KMP-NativeCoroutines.git", exact: "1.0.4")
],
targets: [
.target(
name: "MyTargetName",
dependencies: [
.product(name: "KMPNativeCoroutinesAsync", package: "KMP-NativeCoroutines"),
.product(name: "KMPNativeCoroutinesCombine", package: "KMP-NativeCoroutines"),
.product(name: "KMPNativeCoroutinesRxSwift", package: "KMP-NativeCoroutines")
]
)
]
Or add it in Xcode by going to File > Add Packages... and providing the URL:
https://github.com/rickclephas/KMP-NativeCoroutines.git.
[!NOTE]
The version for the Swift package should not contain the Kotlin version suffix
(e.g. -new-mm or -kotlin-1.6.0).
[!NOTE]
If you only need a single implementation you can also use the SPM specific versions with suffixes
-spm-async, -spm-combine and -spm-rxswift.
Swift (CocoaPods)
If you use CocoaPods add one or more of the following libraries to your Podfile:
pod 'KMPNativeCoroutinesAsync', git: 'https://github.com/rickclephas/KMP-NativeCoroutines.git', tag: 'v1.0.4'
pod 'KMPNativeCoroutinesCombine', git: 'https://github.com/rickclephas/KMP-NativeCoroutines.git', tag:
pod , ,
[!NOTE]
The version for CocoaPods should not contain the Kotlin version suffix (e.g. -new-mm or -kotlin-1.6.0).
IntelliJ / Android Studio
Install the IDE plugin from the JetBrains Marketplace to get:
- Annotation usage validation
- Exposed coroutines warnings
- Quick fixes to add annotations
Usage
Using your Kotlin Coroutines code from Swift is almost as easy as calling the Kotlin code.
Just use the wrapper functions in Swift to get async functions, AsyncStreams, Publishers or Observables.
Kotlin
The plugin will automagically generate the necessary code for you! 🔮
Just annotate your coroutines declarations with @NativeCoroutines (or @NativeCoroutinesState).
Flows
Your Flow properties/functions get a native version:
import com.rickclephas.kmp.nativecoroutines.NativeCoroutines
class Clock {
@NativeCoroutines
val time: StateFlow<Long>
}
StateFlows
Using StateFlow properties to track state (like in a view model)?
Use the @NativeCoroutinesState annotation instead:
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
class Clock {
@NativeCoroutinesState
val time: StateFlow<Long>
}
Generated code
The plugin will generate these native properties for you:
import com.rickclephas.kmp.nativecoroutines.asNativeFlow
import kotlin.native.ObjCName
@ObjCName(name = "time")
val Clock.timeValue
get() = time.value
val Clock.timeFlow
get() = time.asNativeFlow()
Suspend functions
The plugin also generates native versions for your annotated suspend functions:
import com.rickclephas.kmp.nativecoroutines.NativeCoroutines
class RandomLettersGenerator {
@NativeCoroutines
suspend fun getRandomLetters(): String {
}
}
Generated code
The plugin will generate this native function for you:
import com.rickclephas.kmp.nativecoroutines.nativeSuspend
import kotlin.native.ObjCName
@ObjCName(name = "getRandomLetters")
fun RandomLettersGenerator.getRandomLettersNative() =
nativeSuspend { getRandomLetters() }
Interface limitations
Unfortunately extension functions/properties aren't
supported
on Objective-C protocols.
However this limitation can be "overcome" with some Swift magic.
Assuming RandomLettersGenerator is an interface instead of a class we can do the following:
import KMPNativeCoroutinesCore
extension RandomLettersGenerator {
func getRandomLetters() -> NativeSuspend<String, Error, KotlinUnit> {
RandomLettersGeneratorNativeKt.getRandomLetters(self)
}
}
Exposed coroutines checks
When suspend functions and/or Flow declarations are exposed to ObjC/Swift,
the compiler and IDE plugin will produce a warning, reminding you to add one of the KMP-NativeCoroutines annotations.
You can customise the severity of these checks in your build.gradle.kts file:
nativeCoroutines {
exposedSeverity = ExposedSeverity.ERROR
}
Or, if you are not interested in these checks, disable them:
nativeCoroutines {
exposedSeverity = ExposedSeverity.NONE
}
Swift Concurrency
The Async implementation provides some functions to get async Swift functions and AsyncSequences.
Async functions
Use the asyncFunction(for:) function to get an async function that can be awaited:
import KMPNativeCoroutinesAsync
let handle = Task {
do {
let letters = try await asyncFunction(for: randomLettersGenerator.getRandomLetters())
print("Got random letters: \(letters)")
} catch {
print()
}
}
handle.cancel()
or if you don't like these do-catches you can use the asyncResult(for:) function:
import KMPNativeCoroutinesAsync
let result = await asyncResult(for: randomLettersGenerator.getRandomLetters())
if case let .success(letters) = result {
print("Got random letters: \(letters)")
}
for Unit returning functions there is also the asyncError(for:) function:
import KMPNativeCoroutinesAsync
if let error = await asyncError(for: integrationTests.returnUnit()) {
print("Failed with error: \(error)")
}
AsyncSequence
For Flows there is the asyncSequence(for:) function to get an AsyncSequence:
import KMPNativeCoroutinesAsync
let handle = Task {
do {
let sequence = asyncSequence(for: randomLettersGenerator.getRandomLettersFlow())
for try await letters sequence {
()
}
} {
()
}
}
handle.cancel()
Combine
The Combine implementation provides a couple functions to get an AnyPublisher for your Coroutines code.
[!NOTE]
These functions create deferred AnyPublishers.
Meaning every subscription will trigger the collection of the Flow or execution of the suspend function.
[!NOTE]
You must keep a reference to the returned Cancellables otherwise the collection will be cancelled immediately.
Publisher
For your Flows use the createPublisher(for:) function:
import KMPNativeCoroutinesCombine
let publisher = createPublisher(for: clock.time)
let cancellable = publisher.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { value in
print()
}
cancellable.cancel()
You can also use the createPublisher(for:) function for suspend functions that return a Flow:
let publisher = createPublisher(for: randomLettersGenerator.getRandomLettersFlow())
Future
For the suspend functions you should use the createFuture(for:) function:
import KMPNativeCoroutinesCombine
let future = createFuture(for: randomLettersGenerator.getRandomLetters())
let cancellable = future.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { value in
()
}
cancellable.cancel()
RxSwift
The RxSwift implementation provides a couple functions to get an Observable or Single for your Coroutines code.
[!NOTE]
These functions create deferred Observables and Singles.
Meaning every subscription will trigger the collection of the Flow or execution of the suspend function.
Observable
For your Flows use the createObservable(for:) function:
import KMPNativeCoroutinesRxSwift
let observable = createObservable(for: clock.time)
let disposable observable.subscribe(onNext: { value
()
}, onError: { error
()
}, onCompleted: {
()
}, onDisposed: {
()
})
disposable.dispose()
You can also use the createObservable(for:) function for suspend functions that return a Flow:
let observable = createObservable(for: randomLettersGenerator.getRandomLettersFlow())
Single
For the suspend functions you should use the createSingle(for:) function:
import KMPNativeCoroutinesRxSwift
let single = createSingle(for: randomLettersGenerator.getRandomLetters())
let disposable = single.subscribe(onSuccess: { value in
print()
}, onFailure: { error
()
}, onDisposed: {
()
})
disposable.dispose()
Customize
There are a number of ways you can customize the generated Kotlin code.
Name suffix
Don't like the naming of the generated properties/functions?
Specify your own custom suffixes in your build.gradle.kts file:
nativeCoroutines {
suffix = "Native"
fileSuffix = null
flowValueSuffix =
flowReplayCacheSuffix =
stateSuffix =
stateFlowSuffix =
}
CoroutineScope
For more control you can provide a custom CoroutineScope with the NativeCoroutineScope annotation:
import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope
class Clock {
@NativeCoroutineScope
internal val coroutineScope = CoroutineScope(job + Dispatchers.Default)
}
[!NOTE]
Your custom coroutine scope must be either internal or public.
If you don't provide a CoroutineScope the default scope will be used which is defined as:
internal val defaultCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
[!NOTE]
KMP-NativeCoroutines has built-in support for KMP-ObservableViewModel.
Coroutines inside your ViewModel will (by default) use the CoroutineScope from the ViewModelScope.
Ignoring declarations
Use the NativeCoroutinesIgnore annotation to tell the plugin to ignore a property or function:
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore
@NativeCoroutinesIgnore
val ignoredFlowProperty: Flow<Int>
@NativeCoroutinesIgnore
suspend fun ignoredSuspendFunction() { }
Refining declarations in Swift
If for some reason you would like to further refine your Kotlin declarations in Swift, you can use the
NativeCoroutinesRefined and NativeCoroutinesRefinedState annotations.
These will tell the plugin to add the ShouldRefineInSwift
annotation to the generated properties/function.
[!NOTE]
This currently requires a module-wide opt-in to kotlin.experimental.ExperimentalObjCRefinement.
You could for example refine your Flow property to an AnyPublisher property:
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesRefined
class Clock {
@NativeCoroutinesRefined
val time: StateFlow<Long>
}
import KMPNativeCoroutinesCombine
extension Clock {
var time: AnyPublisher<KotlinLong, Error> {
createPublisher(for: __time)
}
}