一个在 KMP 应用中从 Swift 代码使用 Kotlin 协程的库。
KMP 和 Kotlin 协程都很出色,但它们结合使用时存在一些限制。
最重要的限制是取消支持。
Kotlin 挂起函数作为带有完成处理程序 (completion handler) 的函数暴露给 Swift。
这使您可以轻松地从 Swift 代码中使用它们,但它不支持取消。
注意
虽然 Swift 5.5 为 Swift 带来了 async 函数,但这并没有解决这个问题。
为了与 ObjC 的互操作性,所有带有完成处理程序的函数都可以像 async 函数一样调用。
这意味着从 Swift 5.5 开始,您的 Kotlin 挂起函数将看起来像 Swift async 函数。
但这只是语法糖,因此仍然没有取消支持。
除了取消支持之外,ObjC 也不支持协议上的泛型。
因此,所有的 Flow
接口都失去了它们的泛型值类型,这使得它们难以使用。
这个库解决了这两个限制 😄。
该库的最新版本使用 Kotlin 版本 2.1.10
。
也提供与旧版本和/或预览版 Kotlin 兼容的版本
版本 | 版本后缀 | Kotlin | KSP | 协程 |
---|---|---|---|---|
最新 | -kotlin-2.1.20-Beta2 | 2.1.20-Beta2 | 1.0.29 | 1.10.1 |
最新 | 无后缀 | 2.1.10 | 1.0.29 | 1.10.1 |
1.0.0-ALPHA-38 | 无后缀 | 2.1.0 | 1.0.29 | 1.9.0 |
1.0.0-ALPHA-37 | 无后缀 | 2.0.21 | 1.0.25 | 1.9.0 |
1.0.0-ALPHA-36 | 无后缀 | 2.0.20 | 1.0.25 | 1.9.0 |
1.0.0-ALPHA-35 | 无后缀 | 2.0.20 | 1.0.24 | 1.8.1 |
1.0.0-ALPHA-34 | 无后缀 | 2.0.10 | 1.0.24 | 1.8.1 |
1.0.0-ALPHA-33 | 无后缀 | 2.0.0 | 1.0.24 | 1.8.1 |
1.0.0-ALPHA-30 | 无后缀 | 1.9.24 | 1.0.20 | 1.8.1 |
1.0.0-ALPHA-28 | 无后缀 | 1.9.23 | 1.0.20 | 1.8.0 |
1.0.0-ALPHA-25 | 无后缀 | 1.9.22 | 1.0.17 | 1.8.0 |
1.0.0-ALPHA-23 | 无后缀 | 1.9.21 | 1.0.16 | 1.7.3 |
1.0.0-ALPHA-21 | 无后缀 | 1.9.20 | 1.0.14 | 1.7.3 |
1.0.0-ALPHA-18 | 无后缀 | 1.9.10 | 1.0.13 | 1.7.3 |
1.0.0-ALPHA-17 | 无后缀 | 1.9.0 | 1.0.12 | 1.7.3 |
1.0.0-ALPHA-12 | 无后缀 | 1.8.22 | 1.0.11 | 1.7.2 |
1.0.0-ALPHA-10 | 无后缀 | 1.8.21 | 1.0.11 | 1.7.1 |
1.0.0-ALPHA-7 | 无后缀 | 1.8.20 | 1.0.10 | 1.6.4 |
您可以从几种 Swift 实现中选择。
根据不同的实现,您可以支持低至 iOS 9、macOS 10.9、tvOS 9 和 watchOS 3 的版本
实现 | Swift | iOS | macOS | tvOS | watchOS |
---|---|---|---|---|---|
Async | 5.5 | 13.0 | 10.15 | 13.0 | 6.0 |
Combine | 5.0 | 13.0 | 10.15 | 13.0 | 6.0 |
RxSwift | 5.0 | 9.0 | 10.9 | 9.0 | 3.0 |
该库由 Kotlin 和 Swift 两部分组成,您需要将它们添加到您的项目中。
Kotlin 部分可在 Maven Central 上获得,Swift 部分可以通过 CocoaPods 或 Swift Package Manager 安装。
请务必始终对所有库使用相同的版本!
对于 Kotlin,只需将插件添加到您的 build.gradle.kts
中
plugins {
id("com.google.devtools.ksp") version "2.1.10-1.0.29"
id("com.rickclephas.kmp.nativecoroutines") version "1.0.0-ALPHA-39"
}
并确保选择加入实验性的 @ObjCName
注解
kotlin.sourceSets.all {
languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")
}
Swift 实现通过 Swift Package Manager 提供。
只需将其添加到您的 Package.swift
文件中
dependencies: [
.package(url: "https://github.com/rickclephas/KMP-NativeCoroutines.git", exact: "1.0.0-ALPHA-39")
],
targets: [
.target(
name: "MyTargetName",
dependencies: [
// Swift Concurrency implementation
.product(name: "KMPNativeCoroutinesAsync", package: "KMP-NativeCoroutines"),
// Combine implementation
.product(name: "KMPNativeCoroutinesCombine", package: "KMP-NativeCoroutines"),
// RxSwift implementation
.product(name: "KMPNativeCoroutinesRxSwift", package: "KMP-NativeCoroutines")
]
)
]
或者在 Xcode 中通过 File
> Add Packages...
并提供 URL:https://github.com/rickclephas/KMP-NativeCoroutines.git
来添加它。
注意
Swift 包的版本不应包含 Kotlin 版本后缀(例如 -new-mm
或 -kotlin-1.6.0
)。
注意
如果您只需要单个实现,您还可以使用带有后缀 -spm-async
、-spm-combine
和 -spm-rxswift
的 SPM 特定版本。
如果您使用 CocoaPods,请将以下一个或多个库添加到您的 Podfile
pod 'KMPNativeCoroutinesAsync', '1.0.0-ALPHA-39' # Swift Concurrency implementation
pod 'KMPNativeCoroutinesCombine', '1.0.0-ALPHA-39' # Combine implementation
pod 'KMPNativeCoroutinesRxSwift', '1.0.0-ALPHA-39' # RxSwift implementation
注意
CocoaPods 的版本不应包含 Kotlin 版本后缀(例如 -new-mm
或 -kotlin-1.6.0
)。
从 JetBrains Marketplace 安装 IDE 插件 以获得
从 Swift 中使用您的 Kotlin 协程代码几乎与调用 Kotlin 代码一样容易。
只需在 Swift 中使用包装器函数来获取 async 函数、AsyncStreams、Publishers 或 Observables。
该插件将自动为您生成必要的代码!🔮
只需使用 @NativeCoroutines
(或 @NativeCoroutinesState
)注解您的协程声明。
您的 Flow
属性/函数会获得一个原生版本
import com.rickclephas.kmp.nativecoroutines.NativeCoroutines
class Clock {
// Somewhere in your Kotlin code you define a Flow property
// and annotate it with @NativeCoroutines
@NativeCoroutines
val time: StateFlow<Long> // This can be any kind of Flow
}
插件将为您生成此原生属性
import com.rickclephas.kmp.nativecoroutines.asNativeFlow
import kotlin.native.ObjCName
@ObjCName(name = "time")
val Clock.timeNative
get() = time.asNativeFlow()
对于上面定义的 StateFlow
,插件还将生成此 value 属性
val Clock.timeValue
get() = time.value
如果是 SharedFlow
,插件将生成一个 replay cache 属性
val Clock.timeReplayCache
get() = time.replayCache
使用 StateFlow
属性来跟踪状态(例如在 view model 中)?
请改用 @NativeCoroutinesState
注解
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
class Clock {
// Somewhere in your Kotlin code you define a StateFlow property
// and annotate it with @NativeCoroutinesState
@NativeCoroutinesState
val time: StateFlow<Long> // This must be a StateFlow
}
插件将为您生成这些原生属性
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()
插件还会为您的注解挂起函数生成原生版本
import com.rickclephas.kmp.nativecoroutines.NativeCoroutines
class RandomLettersGenerator {
// Somewhere in your Kotlin code you define a suspend function
// and annotate it with @NativeCoroutines
@NativeCoroutines
suspend fun getRandomLetters(): String {
// Code to generate some random letters
}
}
插件将为您生成此原生函数
import com.rickclephas.kmp.nativecoroutines.nativeSuspend
import kotlin.native.ObjCName
@ObjCName(name = "getRandomLetters")
fun RandomLettersGenerator.getRandomLettersNative() =
nativeSuspend { getRandomLetters() }
不幸的是,扩展函数/属性在 Objective-C 协议上是不支持的。
但是,这种限制可以通过一些 Swift 技巧来“克服”。
假设 RandomLettersGenerator
是一个 interface
而不是 class
,我们可以这样做
import KMPNativeCoroutinesCore
extension RandomLettersGenerator {
func getRandomLetters() -> NativeSuspend<String, Error, KotlinUnit> {
RandomLettersGeneratorNativeKt.getRandomLetters(self)
}
}
当挂起函数和/或 Flow
声明暴露给 ObjC/Swift 时,编译器和 IDE 插件将产生警告,提醒您添加 KMP-NativeCoroutines 注解之一。
您可以在您的 build.gradle.kts
文件中自定义这些检查的严重程度
nativeCoroutines {
exposedSeverity = ExposedSeverity.ERROR
}
或者,如果您对这些检查不感兴趣,请禁用它们
nativeCoroutines {
exposedSeverity = ExposedSeverity.NONE
}
Async 实现提供了一些函数来获取 async Swift 函数和 AsyncSequence
s。
使用 asyncFunction(for:)
函数来获取可以被 await 的 async 函数
import KMPNativeCoroutinesAsync
let handle = Task {
do {
let letters = try await asyncFunction(for: randomLettersGenerator.getRandomLetters())
print("Got random letters: \(letters)")
} catch {
print("Failed with error: \(error)")
}
}
// To cancel the suspend function just cancel the async task
handle.cancel()
或者如果您不喜欢这些 do-catches,您可以使用 asyncResult(for:)
函数
import KMPNativeCoroutinesAsync
let result = await asyncResult(for: randomLettersGenerator.getRandomLetters())
if case let .success(letters) = result {
print("Got random letters: \(letters)")
}
对于返回 Unit
的函数,还有 asyncError(for:)
函数
import KMPNativeCoroutinesAsync
if let error = await asyncError(for: integrationTests.returnUnit()) {
print("Failed with error: \(error)")
}
对于 Flow
s,有 asyncSequence(for:)
函数来获取 AsyncSequence
import KMPNativeCoroutinesAsync
let handle = Task {
do {
let sequence = asyncSequence(for: randomLettersGenerator.getRandomLettersFlow())
for try await letters in sequence {
print("Got random letters: \(letters)")
}
} catch {
print("Failed with error: \(error)")
}
}
// To cancel the flow (collection) just cancel the async task
handle.cancel()
Combine 实现提供了一些函数,用于为您的协程代码获取 AnyPublisher
。
注意
这些函数创建延迟的 AnyPublisher
s。
这意味着每次订阅都将触发 Flow
的收集或挂起函数的执行。
注意
您必须保留对返回的 Cancellable
s 的引用,否则收集将立即被取消。
对于您的 Flow
s,请使用 createPublisher(for:)
函数
import KMPNativeCoroutinesCombine
// Create an AnyPublisher for your flow
let publisher = createPublisher(for: clock.time)
// Now use this publisher as you would any other
let cancellable = publisher.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { value in
print("Received value: \(value)")
}
// To cancel the flow (collection) just cancel the publisher
cancellable.cancel()
您还可以对返回 Flow
的挂起函数使用 createPublisher(for:)
函数
let publisher = createPublisher(for: randomLettersGenerator.getRandomLettersFlow())
对于挂起函数,您应该使用 createFuture(for:)
函数
import KMPNativeCoroutinesCombine
// Create a Future/AnyPublisher for the suspend function
let future = createFuture(for: randomLettersGenerator.getRandomLetters())
// Now use this future as you would any other
let cancellable = future.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { value in
print("Received value: \(value)")
}
// To cancel the suspend function just cancel the future
cancellable.cancel()
RxSwift 实现提供了一些函数,用于为您的协程代码获取 Observable
或 Single
。
注意
这些函数创建延迟的 Observable
s 和 Single
s。
这意味着每次订阅都将触发 Flow
的收集或挂起函数的执行。
对于您的 Flow
s,请使用 createObservable(for:)
函数
import KMPNativeCoroutinesRxSwift
// Create an observable for your flow
let observable = createObservable(for: clock.time)
// Now use this observable as you would any other
let disposable = observable.subscribe(onNext: { value in
print("Received value: \(value)")
}, onError: { error in
print("Received error: \(error)")
}, onCompleted: {
print("Observable completed")
}, onDisposed: {
print("Observable disposed")
})
// To cancel the flow (collection) just dispose the subscription
disposable.dispose()
您还可以对返回 Flow
的挂起函数使用 createObservable(for:)
函数
let observable = createObservable(for: randomLettersGenerator.getRandomLettersFlow())
对于挂起函数,您应该使用 createSingle(for:)
函数
import KMPNativeCoroutinesRxSwift
// Create a single for the suspend function
let single = createSingle(for: randomLettersGenerator.getRandomLetters())
// Now use this single as you would any other
let disposable = single.subscribe(onSuccess: { value in
print("Received value: \(value)")
}, onFailure: { error in
print("Received error: \(error)")
}, onDisposed: {
print("Single disposed")
})
// To cancel the suspend function just dispose the subscription
disposable.dispose()
您可以通过多种方式自定义生成的 Kotlin 代码。
不喜欢生成的属性/函数的命名?
在您的 build.gradle.kts
文件中指定您自己的自定义后缀
nativeCoroutines {
// The suffix used to generate the native coroutine function and property names.
suffix = "Native"
// The suffix used to generate the native coroutine file names.
// Note: defaults to the suffix value when `null`.
fileSuffix = null
// The suffix used to generate the StateFlow value property names,
// or `null` to remove the value properties.
flowValueSuffix = "Value"
// The suffix used to generate the SharedFlow replayCache property names,
// or `null` to remove the replayCache properties.
flowReplayCacheSuffix = "ReplayCache"
// The suffix used to generate the native state property names.
stateSuffix = "Value"
// The suffix used to generate the `StateFlow` flow property names,
// or `null` to remove the flow properties.
stateFlowSuffix = "Flow"
}
为了获得更多控制,您可以使用带有 NativeCoroutineScope
注解的自定义 CoroutineScope
import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope
class Clock {
@NativeCoroutineScope
internal val coroutineScope = CoroutineScope(job + Dispatchers.Default)
}
注意
您的自定义协程作用域必须是 internal
或 public
。
如果您不提供 CoroutineScope
,将使用默认作用域,其定义为
internal val defaultCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
注意
KMP-NativeCoroutines 内置支持 KMP-ObservableViewModel。ViewModel
内部的协程将(默认情况下)使用来自 ViewModelScope
的 CoroutineScope
。
使用 NativeCoroutinesIgnore
注解来告诉插件忽略属性或函数
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore
@NativeCoroutinesIgnore
val ignoredFlowProperty: Flow<Int>
@NativeCoroutinesIgnore
suspend fun ignoredSuspendFunction() { }
如果出于某种原因您想在 Swift 中进一步精炼您的 Kotlin 声明,您可以使用 NativeCoroutinesRefined
和 NativeCoroutinesRefinedState
注解。
这些注解会告诉插件将 ShouldRefineInSwift
注解添加到生成的属性/函数。
注意
目前,这需要模块范围选择加入 kotlin.experimental.ExperimentalObjCRefinement
。
例如,您可以将您的 Flow
属性精炼为 AnyPublisher
属性
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)
}
}