Une bibliothèque pour utiliser Kotlin Coroutines à partir de code Swift dans les applications KMP.
KMP et Kotlin Coroutines sont incroyables, mais ensemble, ils ont certaines limites.
La limitation la plus importante est le support d'annulation.
Les fonctions de suspension de Kotlin sont exposées à Swift en tant que fonctions avec un gestionnaire d'achèvement.
Cela vous permet de les utiliser facilement à partir de votre code Swift, mais il ne prend pas en charge l'annulation.
Note
Bien que Swift 5.5 apporte des fonctions asynchrones à Swift, elle ne résout pas ce problème.
Pour l'interopérabilité avec OBJC, toutes les fonctions avec un gestionnaire d'achèvement peuvent être appelées comme une fonction asynchrone.
Cela signifie commencer par Swift 5.5 Vos fonctions de suspension Kotlin ressembleront à des fonctions asynchrones rapides.
Mais c'est juste du sucre syntaxique, donc il n'y a toujours pas de support d'annulation.
Outre le support d'annulation, l'OBJC ne prend pas en charge les génériques sur les protocoles.
Ainsi, toutes les interfaces Flow perdent leur type de valeur générique qui les rend difficiles à utiliser.
Cette bibliothèque résout ces deux limitations ?.
La dernière version de la bibliothèque utilise Kotlin version 2.1.0 .
Des versions de compatibilité pour les versions Kotlin anciennes et / ou prévisuales sont également disponibles:
| Version | Suffixe de version | Kotlin | Ksp | Coroutines |
|---|---|---|---|---|
| dernier | pas de suffixe | 2.1.0 | 1.0.29 | 1.9.0 |
| 1.0.0-alpha-37 | pas de suffixe | 2.0.21 | 1.0.25 | 1.9.0 |
| 1.0.0-alpha-36 | pas de suffixe | 2.0.20 | 1.0.25 | 1.9.0 |
| 1.0.0-alpha-35 | pas de suffixe | 2.0.20 | 1.0.24 | 1.8.1 |
| 1.0.0-alpha-34 | pas de suffixe | 2.0.10 | 1.0.24 | 1.8.1 |
| 1.0.0-alpha-33 | pas de suffixe | 2.0.0 | 1.0.24 | 1.8.1 |
| 1.0.0-alpha-30 | pas de suffixe | 1.9.24 | 1.0.20 | 1.8.1 |
| 1.0.0-alpha-28 | pas de suffixe | 1.9.23 | 1.0.20 | 1.8.0 |
| 1.0.0-alpha-25 | pas de suffixe | 1.9.22 | 1.0.17 | 1.8.0 |
| 1.0.0-alpha-23 | pas de suffixe | 1.9.21 | 1.0.16 | 1.7.3 |
| 1.0.0-alpha-21 | pas de suffixe | 1.9.20 | 1.0.14 | 1.7.3 |
| 1.0.0-alpha-18 | pas de suffixe | 1.9.10 | 1.0.13 | 1.7.3 |
| 1.0.0-alpha-17 | pas de suffixe | 1.9.0 | 1.0.12 | 1.7.3 |
| 1.0.0-alpha-12 | pas de suffixe | 1.8.22 | 1.0.11 | 1.7.2 |
| 1.0.0-alpha-10 | pas de suffixe | 1.8.21 | 1.0.11 | 1.7.1 |
| 1.0.0-alpha-7 | pas de suffixe | 1.8.20 | 1.0.10 | 1.6.4 |
Vous pouvez choisir parmi quelques implémentations Swift.
Selon l'implémentation que vous pouvez prendre en charge aussi faible que iOS 9, MacOS 10.9, TVOS 9 et Watchos 3:
| Mise en œuvre | Rapide | ios | macos | tvos | watchos |
|---|---|---|---|---|---|
| Asynchrone | 5.5 | 13.0 | 10.15 | 13.0 | 6.0 |
| Combiner | 5.0 | 13.0 | 10.15 | 13.0 | 6.0 |
| Rxswift | 5.0 | 9.0 | 10.9 | 9.0 | 3.0 |
La bibliothèque se compose d'une partie kotlin et rapide que vous devrez ajouter à votre projet.
La pièce Kotlin est disponible sur Maven Central et la pièce Swift peut être installée via Cocoapods ou le Swift Package Manager.
Assurez-vous de toujours utiliser les mêmes versions pour toutes les bibliothèques!
Pour Kotlin, ajoutez simplement le plugin à votre 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 "
} Et assurez-vous de vous opposer à l'annotation expérimentale @ObjCName :
kotlin.sourceSets.all {
languageSettings.optIn( " kotlin.experimental.ExperimentalObjCName " )
} Les implémentations SWIFT sont disponibles via le gestionnaire de packages Swift.
Ajoutez-le simplement à votre fichier 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 " )
]
)
] Ou ajoutez-le dans Xcode en allant à File > Add Packages... et en fournissant l'URL: https://github.com/rickclephas/KMP-NativeCoroutines.git .
Note
La version du package Swift ne doit pas contenir le suffixe de la version Kotlin (par exemple -new-mm ou -kotlin-1.6.0 ).
Note
Si vous n'avez besoin que d'une seule implémentation, vous pouvez également utiliser les versions spécifiques à SPM avec des suffixes -spm-async , -spm-combine et -spm-rxswift .
Si vous utilisez des cocoapodes, ajoutez une ou plusieurs des bibliothèques suivantes à votre 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 Note
La version de Cocoapods ne doit pas contenir le suffixe de la version Kotlin (par exemple -new-mm ou -kotlin-1.6.0 ).
Installez le plugin IDE du JetBrains Marketplace pour obtenir:
L'utilisation de votre code Kotlin Coroutines de Swift est presque aussi simple que d'appeler le code Kotlin.
Utilisez simplement les fonctions de wrapper dans Swift pour obtenir des fonctions asynchrones, asynchrones, éditeurs ou observables.
Le plugin générera automatiquement le code nécessaire pour vous! ?
Annotez simplement vos déclarations Coroutines avec @NativeCoroutines (ou @NativeCoroutinesState ).
Vos propriétés / fonctions Flow obtiennent une version native:
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
}Le plugin générera cette propriété native pour vous:
import com.rickclephas.kmp.nativecoroutines.asNativeFlow
import kotlin.native.ObjCName
@ObjCName(name = " time " )
val Clock .timeNative
get() = time.asNativeFlow() Pour le StateFlow défini au-dessus du plugin générera également cette propriété de valeur:
val Clock .timeValue
get() = time.value En cas de SharedFlow le plugin générerait une propriété de cache de relecture:
val Clock .timeReplayCache
get() = time.replayCache Utilisation des propriétés StateFlow pour suivre l'état (comme dans un modèle de vue)?
Utilisez à la place l'annotation @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
}Le plugin générera ces propriétés natives pour vous:
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()Le plugin génère également des versions natives pour vos fonctions de suspension annotées:
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
}
}Le plugin générera cette fonction native pour vous:
import com.rickclephas.kmp.nativecoroutines.nativeSuspend
import kotlin.native.ObjCName
@ObjCName(name = " getRandomLetters " )
fun RandomLettersGenerator. getRandomLettersNative () =
nativeSuspend { getRandomLetters() }Malheureusement, les fonctions / propriétés d'extension ne sont pas prises en charge sur les protocoles Objective-C.
Cependant, cette limitation peut être "surmontée" avec une magie rapide.
En supposant que RandomLettersGenerator est une interface au lieu d'une class , nous pouvons faire ce qui suit:
import KMPNativeCoroutinesCore
extension RandomLettersGenerator {
func getRandomLetters ( ) -> NativeSuspend < String , Error , KotlinUnit > {
RandomLettersGeneratorNativeKt . getRandomLetters ( self )
}
} Lorsque les fonctions de suspension et / ou les déclarations Flow sont exposées à OBJC / SWIFT, le compilateur et le plugin IDE produiront un avertissement, vous rappelant d'ajouter l'une des annotations KMP-NativeCoroutines.
Vous pouvez personnaliser la gravité de ces chèques dans votre fichier build.gradle.kts :
nativeCoroutines {
exposedSeverity = ExposedSeverity . ERROR
}Ou, si vous n'êtes pas intéressé par ces chèques, désactivez-les:
nativeCoroutines {
exposedSeverity = ExposedSeverity . NONE
} L'implémentation asynchrone fournit certaines fonctions pour obtenir des fonctions Swift asynchrones et des AsyncSequence .
Utilisez la fonction asyncFunction(for:) pour obtenir une fonction asynchrone qui peut être attendue:
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 ( ) Ou si vous n'aimez pas ces cas-catchés, vous pouvez utiliser l' asyncResult(for:) Fonction:
import KMPNativeCoroutinesAsync
let result = await asyncResult ( for : randomLettersGenerator . getRandomLetters ( ) )
if case let . success ( letters ) = result {
print ( " Got random letters: ( letters ) " )
} Pour les fonctions de retour Unit , il y a aussi l' asyncError(for:) fonction:
import KMPNativeCoroutinesAsync
if let error = await asyncError ( for : integrationTests . returnUnit ( ) ) {
print ( " Failed with error: ( error ) " )
} Pour Flow s, il y a la fonction asyncSequence(for:) pour obtenir une 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 ( ) L'implémentation Combine fournit quelques fonctions pour obtenir un AnyPublisher pour votre code Coroutines.
Note
Ces fonctions créent AnyPublisher différées.
Cela signifie que chaque abonnement déclenchera la collecte de l' Flow ou de l'exécution de la fonction de suspension.
Note
Vous devez conserver une référence à la Cancellable renvoyée S sinon la collection sera annulée immédiatement.
Pour votre Flow , utilisez le createPublisher(for:) Fonction:
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 ( ) Vous pouvez également utiliser la fonction createPublisher(for:) pour suspendre les fonctions qui renvoient un Flow :
let publisher = createPublisher ( for : randomLettersGenerator . getRandomLettersFlow ( ) ) Pour les fonctions de suspension, vous devez utiliser la createFuture(for:) Fonction:
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 ( ) L'implémentation RXSWIFT fournit un couple de fonctions pour obtenir un Observable ou Single pour votre code Coroutines.
Note
Ces fonctions créent des S Observable différés et Single S.
Cela signifie que chaque abonnement déclenchera la collecte de l' Flow ou de l'exécution de la fonction de suspension.
Pour votre Flow , utilisez la fonction createObservable(for:) Fonction:
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 ( ) Vous pouvez également utiliser la fonction createObservable(for:) pour suspendre les fonctions qui renvoient un Flow :
let observable = createObservable ( for : randomLettersGenerator . getRandomLettersFlow ( ) ) Pour les fonctions de suspension, vous devez utiliser la fonction 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 ( ) Il existe plusieurs façons de personnaliser le code Kotlin généré.
Vous n'aimez pas la dénomination des propriétés / fonctions générées?
Spécifiez vos propres suffixes personnalisés dans votre fichier 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 "
} Pour plus de contrôle, vous pouvez fournir un CoroutineScope personnalisé avec l'annotation NativeCoroutineScope :
import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope
class Clock {
@NativeCoroutineScope
internal val coroutineScope = CoroutineScope (job + Dispatchers . Default )
}Note
Votre portée de coroutine personnalisée doit être internal ou public .
Si vous ne fournissez pas de CoroutineScope , la portée par défaut sera utilisée qui est définie comme:
internal val defaultCoroutineScope = CoroutineScope ( SupervisorJob () + Dispatchers . Default )Note
KMP-NativeCoroutines a une prise en charge intégrée pour KMP-ObservableViewModel.
Les coroutines à l'intérieur de votre ViewModel utiliseront (par défaut) le CoroutineScope à partir du ViewModelScope .
Utilisez l'annotation NativeCoroutinesIgnore pour dire au plugin d'ignorer une propriété ou une fonction:
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore
@NativeCoroutinesIgnore
val ignoredFlowProperty : Flow < Int >
@NativeCoroutinesIgnore
suspend fun ignoredSuspendFunction () { } Si, pour une raison quelconque, vous souhaitez affiner davantage vos déclarations de Kotlin dans Swift, vous pouvez utiliser les annotations NativeCoroutinesRefined et NativeCoroutinesRefinedState .
Ceux-ci diront au plugin d'ajouter l'annotation de ShouldRefineInSwift aux propriétés / fonction générées.
Note
Cela nécessite actuellement une opt-in de module à kotlin.experimental.ExperimentalObjCRefinement .
Vous pouvez par exemple affiner votre propriété Flow vers une propriété 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 )
}
}