Библиотека для использования Kotlin Coroutines из Swift Code в приложениях KMP.
Кораки KMP и Kotlin удивительны, но вместе у них есть некоторые ограничения.
Наиболее важным ограничением является поддержка отмены.
Функции приостановки Kotlin подвергаются воздействию Swift как функции с обработчиком завершения.
Это позволяет легко использовать их из вашего Swift Code, но не поддерживает отмену.
Примечание
В то время как Swift 5.5 приносит асинхронные функции в Swift, это не решает эту проблему.
Для совместимости с OBJC все функции с обработчиком завершения можно назвать как асинхронная функция.
Это означает, что начиная с Swift 5.5, ваши функции подвески Kotlin будут выглядеть как Swift Async Functs.
Но это просто синтаксический сахар, поэтому до сих пор нет поддержки отмены.
Помимо поддержки отмены, OBJC не поддерживает дженерики по протоколам.
Таким образом, все интерфейсы Flow теряют свой общий тип значения, который затрудняет их использование.
Эта библиотека решает оба этих ограничения?
Последняя версия библиотеки использует Kotlin версию 2.1.0 .
Также доступны версии совместимости для старых и/или предварительного просмотра версий Kotlin:
| Версия | Версия суффикс | Котлин | Ksp | Кораки |
|---|---|---|---|---|
| последний | Нет суффикса | 2.1.0 | 1.0.29 | 1.9.0 |
| 1.0.0-Альфа-37 | Нет суффикса | 2.0.21 | 1.0.25 | 1.9.0 |
| 1.0.0-альфа-36 | Нет суффикса | 2.0.20 | 1.0.25 | 1.9.0 |
| 1.0.0-альфа-35 | Нет суффикса | 2.0.20 | 1.0.24 | 1.8.1 |
| 1.0.0-Альфа-34 | Нет суффикса | 2.0.10 | 1.0.24 | 1.8.1 |
| 1.0.0-Альфа-33 | Нет суффикса | 2.0.0 | 1.0.24 | 1.8.1 |
| 1.0.0-Альфа-30 | Нет суффикса | 1.9.24 | 1.0.20 | 1.8.1 |
| 1.0.0-альфа-28 | Нет суффикса | 1.9.23 | 1.0.20 | 1.8.0 |
| 1.0.0-альфа-25 | Нет суффикса | 1.9.22 | 1.0.17 | 1.8.0 |
| 1.0.0-альфа-23 | Нет суффикса | 1.9.21 | 1.0.16 | 1.7.3 |
| 1.0.0-альфа-21 | Нет суффикса | 1.9.20 | 1.0.14 | 1.7.3 |
| 1.0.0-альфа-18 | Нет суффикса | 1.9.10 | 1.0.13 | 1.7.3 |
| 1.0.0-альфа-17 | Нет суффикса | 1.9.0 | 1.0.12 | 1.7.3 |
| 1.0.0-альфа-12 | Нет суффикса | 1.8.22 | 1.0.11 | 1.7.2 |
| 1.0.0-альфа-10 | Нет суффикса | 1.8.21 | 1.0.11 | 1.7.1 |
| 1.0.0-Альфа-7 | Нет суффикса | 1.8.20 | 1.0.10 | 1.6.4 |
Вы можете выбрать из нескольких реализаций Swift.
В зависимости от реализации вы можете поддерживать такую же низкую, как iOS 9, MacOS 10.9, TVOS 9 и WatchOS 3:
| Выполнение | Быстрый | ios | macOS | TVOS | ВОЗДА |
|---|---|---|---|---|---|
| Асинхро | 5.5 | 13.0 | 10.15 | 13.0 | 6,0 |
| Комбинировать | 5.0 | 13.0 | 10.15 | 13.0 | 6,0 |
| Rxswift | 5.0 | 9.0 | 10.9 | 9.0 | 3.0 |
Библиотека состоит из котлин и быстрой части, которую вам нужно добавить в свой проект.
Часть Kotlin доступна на Maven Central, и Swift Part может быть установлена через Cocoapods или Swift Package Manager.
Обязательно используйте одни и те же версии для всех библиотек!
Для Kotlin просто добавьте плагин в свой build.gradle.kts :
plugins {
id( " com.google.devtools.ksp " ) version " 2.1.0-1.0.29 "
id( " com.rickclephas.kmp.nativecoroutines " ) version " 1.0.0-ALPHA-38 "
} И обязательно выберите экспериментальную аннотацию @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-38 " )
] ,
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 с суффиксами -spm-async , -spm-combine и -spm-rxswift .
Если вы используете кокопод, добавьте одну или несколько из следующих библиотек в свой Podfile :
pod 'KMPNativeCoroutinesAsync' , '1.0.0-ALPHA-38' # Swift Concurrency implementation
pod 'KMPNativeCoroutinesCombine' , '1.0.0-ALPHA-38' # Combine implementation
pod 'KMPNativeCoroutinesRxSwift' , '1.0.0-ALPHA-38' # RxSwift implementation Примечание
Версия для кокопод не должна содержать суффикс версии Kotlin (например, -new-mm или -kotlin-1.6.0 ).
Установите плагин IDE с рынка JetBrains, чтобы получить:
Использование кода Kotlin Coroutines из Swift почти так же просто, как вызов кода Kotlin.
Просто используйте функции обертки в Swift, чтобы получить асинхронные функции, Asyncstream, издатели или наблюдаемые.
Плагин автоматически генерирует необходимый код для вас! ?
Просто аннотируйте свои декларации Coroutines @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 определенная выше плагина, также будет генерировать это свойство значение:
val Clock .timeValue
get() = time.value В случае SharedFlow плагин генерирует свойство кэша воспроизведения:
val Clock .timeReplayCache
get() = time.replayCache Использование свойств StateFlow для отслеживания состояния (например, в модели просмотра)?
Вместо этого используйте аннотацию @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
} Асинхронная реализация предоставляет некоторые функции для получения асинхронных функций и AsyncSequence .
Используйте функцию asyncFunction(for:)
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 ( ) Или, если вам не нравятся эти качки, вы можете использовать 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 для вашего кода Coroutines.
Примечание
Эти функции создают отложенные AnyPublisher .
Это означает, что каждая подписка запустит сбор Flow или выполнения функции подвески.
Примечание
Вы должны соблюдать ссылку на возвращенную Cancellable S, в противном случае коллекция будет немедленно отменена.
Для вашего Flow используйте 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 ( ) Вы также можете использовать createPublisher(for:) Функция для подвесных функций, которые возвращают Flow :
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 для вашего кода CORUTINES.
Примечание
Эти функции создают отложенные Observable S и Single S.
Это означает, что каждая подписка запустит сбор Flow или выполнения функции подвески.
Для вашего Flow используйте 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 ( ) Вы также можете использовать функцию createObservable(for:) для подвесных функций, которые возвращают Flow :
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 ( ) Есть несколько способов настроить сгенерированный код котлина.
Не нравится именование сгенерированных свойств/функций?
Укажите свои собственные суффиксы в вашем файле 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 "
} Для получения дополнительного контроля вы можете предоставить пользовательский CoroutineScope с аннотацией NativeCoroutineScope :
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.
Coroutines внутри вашей ViewModel (по умолчанию) использует CoroutineScope из ViewModelScope .
Используйте аннотацию NativeCoroutinesIgnore , чтобы сообщить плагину игнорировать свойство или функцию:
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore
@NativeCoroutinesIgnore
val ignoredFlowProperty : Flow < Int >
@NativeCoroutinesIgnore
suspend fun ignoredSuspendFunction () { } Если по какой -то причине вы хотели бы дополнительно уточнить свои декларации Kotlin в Swift, вы можете использовать Annotations 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 )
}
}