Una biblioteca para usar Kotlin Coroutines de Swift Code en las aplicaciones KMP.
Tanto KMP como Kotlin Coroutines son increíbles, pero juntos tienen algunas limitaciones.
La limitación más importante es el soporte de cancelación.
Las funciones de Kotlin Suspend están expuestas a Swift como funciones con un controlador de finalización.
Esto le permite usarlos fácilmente desde su código rápido, pero no admite la cancelación.
Nota
Si bien Swift 5.5 trae funciones de Async a Swift, no resuelve este problema.
Para la interoperabilidad con OBJC, todas las funciones con un controlador de finalización pueden llamarse como una función asíncrata.
Esto significa que comenzar con Swift 5.5 Sus funciones Kotlin Suspend se verán como funciones de asíncrono Swift.
Pero eso es solo azúcar sintáctica, por lo que todavía no hay soporte de cancelación.
Además del soporte de cancelación, OBJC no admite genéricos en protocolos.
Por lo tanto, todas las interfaces Flow pierden su tipo de valor genérico que las hace difíciles de usar.
¿Esta biblioteca resuelve ambas limitaciones?
La última versión de la biblioteca usa Kotlin versión 2.1.0 .
Las versiones de compatibilidad para versiones de Kotlin más antiguas y/o previas también están disponibles:
| Versión | Versión sufijo | Kotlín | KSP | Corutina |
|---|---|---|---|---|
| el último | sin sufijo | 2.1.0 | 1.0.29 | 1.9.0 |
| 1.0.0-alfa-37 | sin sufijo | 2.0.21 | 1.0.25 | 1.9.0 |
| 1.0.0-alfa-36 | sin sufijo | 2.0.20 | 1.0.25 | 1.9.0 |
| 1.0.0-Alpha-35 | sin sufijo | 2.0.20 | 1.0.24 | 1.8.1 |
| 1.0.0-alfa-34 | sin sufijo | 2.0.10 | 1.0.24 | 1.8.1 |
| 1.0.0-Alpha-33 | sin sufijo | 2.0.0 | 1.0.24 | 1.8.1 |
| 1.0.0-alfa-30 | sin sufijo | 1.9.24 | 1.0.20 | 1.8.1 |
| 1.0.0-alfa-28 | sin sufijo | 1.9.23 | 1.0.20 | 1.8.0 |
| 1.0.0-alfa-25 | sin sufijo | 1.9.22 | 1.0.17 | 1.8.0 |
| 1.0.0-alfa-23 | sin sufijo | 1.9.21 | 1.0.16 | 1.7.3 |
| 1.0.0-alfa-21 | sin sufijo | 1.9.20 | 1.0.14 | 1.7.3 |
| 1.0.0-alfa-18 | sin sufijo | 1.9.10 | 1.0.13 | 1.7.3 |
| 1.0.0-alfa-17 | sin sufijo | 1.9.0 | 1.0.12 | 1.7.3 |
| 1.0.0-alfa-12 | sin sufijo | 1.8.22 | 1.0.11 | 1.7.2 |
| 1.0.0-alfa-10 | sin sufijo | 1.8.21 | 1.0.11 | 1.7.1 |
| 1.0.0-alfa-7 | sin sufijo | 1.8.20 | 1.0.10 | 1.6.4 |
Puede elegir entre un par de implementaciones rápidas.
Dependiendo de la implementación que pueda admitir tan baja como iOS 9, MacOS 10.9, Tvos 9 y WatchOS 3:
| Implementación | Rápido | iOS | macosa | tvos | vigilancia |
|---|---|---|---|---|---|
| Asíncrata | 5.5 | 13.0 | 10.15 | 13.0 | 6.0 |
| Combinar | 5.0 | 13.0 | 10.15 | 13.0 | 6.0 |
| Rxswift | 5.0 | 9.0 | 10.9 | 9.0 | 3.0 |
La biblioteca consiste en una parte Kotlin y Swift que necesitará agregar a su proyecto.
La parte Kotlin está disponible en Maven Central y la parte Swift se puede instalar a través de Cocoapods o el Swift Package Manager.
¡Asegúrese de usar siempre las mismas versiones para todas las bibliotecas!
Para Kotlin, solo agregue el complemento a su 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 "
} y asegúrese de optar por la anotación experimental @ObjCName :
kotlin.sourceSets.all {
languageSettings.optIn( " kotlin.experimental.ExperimentalObjCName " )
} Las implementaciones de Swift están disponibles a través del Swift Package Manager.
Simplemente agréguelo a su archivo 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 " )
]
)
] O agréguelo en xcode yendo a File > Add Packages... y proporcionar la URL: https://github.com/rickclephas/KMP-NativeCoroutines.git .
Nota
La versión para el paquete Swift no debe contener el sufijo de la versión Kotlin (por ejemplo, -new-mm o -kotlin-1.6.0 ).
Nota
Si solo necesita una sola implementación, también puede usar las versiones específicas de SPM con sufijos -spm-async , -spm-combine y -spm-rxswift .
Si usa cocoapods, agregue una o más de las siguientes bibliotecas a su 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 Nota
La versión para Cocoapods no debe contener el sufijo de la versión Kotlin (por ejemplo, -new-mm o -kotlin-1.6.0 ).
Instale el complemento IDE desde el mercado de JetBrains para obtener:
Usar su código Kotlin Coroutines de Swift es casi tan fácil como llamar al código Kotlin.
Simplemente use las funciones de envoltura en Swift para obtener funciones de async, asyncstreams, editores u observables.
¡El complemento generará automáticamente el código necesario para usted! ?
Simplemente anote sus declaraciones de coroutines con @NativeCoroutines (o @NativeCoroutinesState ).
Sus propiedades/funciones Flow obtienen una versión nativa:
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
}El complemento generará esta propiedad nativa para usted:
import com.rickclephas.kmp.nativecoroutines.asNativeFlow
import kotlin.native.ObjCName
@ObjCName(name = " time " )
val Clock .timeNative
get() = time.asNativeFlow() Para el StateFlow definido anteriormente, el complemento también generará esta propiedad de valor:
val Clock .timeValue
get() = time.value En el caso de un SharedFlow , el complemento generaría una propiedad de caché de repetición:
val Clock .timeReplayCache
get() = time.replayCache ¿Usar las propiedades StateFlow para rastrear el estado (como en un modelo de vista)?
Use la anotación @NativeCoroutinesState en su lugar:
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
}El complemento generará estas propiedades nativas para usted:
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()El complemento también genera versiones nativas para sus funciones de suspensión anotadas:
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
}
}El complemento generará esta función nativa para usted:
import com.rickclephas.kmp.nativecoroutines.nativeSuspend
import kotlin.native.ObjCName
@ObjCName(name = " getRandomLetters " )
fun RandomLettersGenerator. getRandomLettersNative () =
nativeSuspend { getRandomLetters() }Desafortunadamente, las funciones/propiedades de extensión no son compatibles con los protocolos Objective-C.
Sin embargo, esta limitación se puede "superar" con algo de magia rápida.
Asumir que RandomLettersGenerator es una interface en lugar de una class , podemos hacer lo siguiente:
import KMPNativeCoroutinesCore
extension RandomLettersGenerator {
func getRandomLetters ( ) -> NativeSuspend < String , Error , KotlinUnit > {
RandomLettersGeneratorNativeKt . getRandomLetters ( self )
}
} Cuando las funciones de suspensión y/o declaraciones Flow están expuestas a OBJC/SWIFT, el complemento del compilador y el IDE producirán una advertencia, recordándole que agregue una de las anotaciones KMP-Nativecoroutines.
Puede personalizar la gravedad de estos cheques en su archivo build.gradle.kts :
nativeCoroutines {
exposedSeverity = ExposedSeverity . ERROR
}O, si no está interesado en estos cheques, desactívelos:
nativeCoroutines {
exposedSeverity = ExposedSeverity . NONE
} La implementación de Async proporciona algunas funciones para obtener funciones de Async Swift y AsyncSequence s.
Use la función asyncFunction(for:) para obtener una función async que se puede esperar:
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 ( ) O si no le gustan estos capches, puede usar asyncResult(for:) función:
import KMPNativeCoroutinesAsync
let result = await asyncResult ( for : randomLettersGenerator . getRandomLetters ( ) )
if case let . success ( letters ) = result {
print ( " Got random letters: ( letters ) " )
} Para las funciones de regreso Unit también existe el asyncError(for:) Función:
import KMPNativeCoroutinesAsync
if let error = await asyncError ( for : integrationTests . returnUnit ( ) ) {
print ( " Failed with error: ( error ) " )
} Para Flow s existe la función asyncSequence(for:) para obtener una 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 ( ) La implementación Combine proporciona un par de funciones para obtener una AnyPublisher para su código Coroutines.
Nota
Estas funciones crean AnyPublisher s.
Lo que significa que cada suscripción activará la recopilación del Flow o ejecución de la función de suspensión.
Nota
Debe mantener una referencia a las Cancellable devueltas, de lo contrario, la colección se cancelará de inmediato.
Para su Flow , use el createPublisher(for:) función:
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 ( ) También puede usar el createPublisher(for:) función para suspender funciones que devuelven un Flow :
let publisher = createPublisher ( for : randomLettersGenerator . getRandomLettersFlow ( ) ) Para las funciones de suspensión, debe usar createFuture(for:) Función:
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 ( ) La implementación de RXSWIFT proporciona un par de funciones para obtener un código Observable o Single para su Coroutines.
Nota
Estas funciones crean s y syle s Observable Single .
Lo que significa que cada suscripción activará la recopilación del Flow o ejecución de la función de suspensión.
Para su Flow , use la función 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 ( ) También puede usar la función createObservable(for:) para suspender funciones que devuelven un Flow :
let observable = createObservable ( for : randomLettersGenerator . getRandomLettersFlow ( ) ) Para las funciones de suspensión, debe usar el createSingle(for:) función:
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 ( ) Hay varias formas en que puede personalizar el código Kotlin generado.
¿No te gusta el nombramiento de las propiedades/funciones generadas?
Especifique sus propios sufijos personalizados en su archivo 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 "
} Para obtener más control, puede proporcionar un CoroutineScope personalizado con la anotación NativeCoroutineScope :
import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope
class Clock {
@NativeCoroutineScope
internal val coroutineScope = CoroutineScope (job + Dispatchers . Default )
}Nota
Su alcance de Coroutine personalizado debe ser internal o public .
Si no proporciona un CoroutineScope , se utilizará el alcance predeterminado que se define como:
internal val defaultCoroutineScope = CoroutineScope ( SupervisorJob () + Dispatchers . Default )Nota
KMP-Nativecoroutines tiene soporte incorporado para KMP-ObservableViewModel.
Las coroutinas dentro de su ViewModel (de forma predeterminada) usará el CoroutineScope desde ViewModelScope .
Use la anotación NativeCoroutinesIgnore para decirle al complemento que ignore una propiedad o función:
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore
@NativeCoroutinesIgnore
val ignoredFlowProperty : Flow < Int >
@NativeCoroutinesIgnore
suspend fun ignoredSuspendFunction () { } Si, por alguna razón, desea refinar aún más sus declaraciones de Kotlin en Swift, puede usar las anotaciones NativeCoroutinesRefined y NativeCoroutinesRefinedState .
Estos le dirán al complemento que agregue la anotación de ShouldRefineInSwift a las propiedades/función generadas.
Nota
Actualmente, esto requiere una opción en todo el módulo a kotlin.experimental.ExperimentalObjCRefinement .
Por ejemplo, podría refinar su propiedad Flow a una propiedad 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 )
}
}