Uma biblioteca para usar o Kotlin Coroutines do código SWIFT em aplicativos KMP.
As coroutinas KMP e Kotlin são incríveis, mas juntas elas têm algumas limitações.
A limitação mais importante é o suporte ao cancelamento.
As funções de suspensão de Kotlin são expostas a Swift como funções com um manipulador de conclusão.
Isso permite que você os use facilmente do seu código SWIFT, mas não suporta o cancelamento.
Observação
Embora o Swift 5.5 traga funções assíncronas para Swift, ele não resolve esse problema.
Para interoperabilidade com o OBJC, todas as funções com um manipulador de conclusão podem ser chamadas como uma função assíncrona.
Isso significa começar com o Swift 5.5 Suas funções de suspensão do Kotlin parecerão funções assíncronas SWIFT.
Mas isso é apenas açúcar sintático, então ainda não há suporte para cancelamento.
Além do suporte ao cancelamento, o OBJC não suporta genéricos em protocolos.
Portanto, todas as interfaces Flow perdem seu tipo de valor genérico, o que as torna difíceis de usar.
Esta biblioteca resolve essas duas limitações?
A versão mais recente da biblioteca usa o Kotlin versão 2.1.0 .
Versões de compatibilidade para versões Kotlin mais antigas e/ou de visualização também estão disponíveis:
| Versão | Sufixo da versão | Kotlin | KSP | Coroutinas |
|---|---|---|---|---|
| mais recente | sem sufixo | 2.1.0 | 1.0.29 | 1.9.0 |
| 1.0.0-alfa-37 | sem sufixo | 2.0.21 | 1.0.25 | 1.9.0 |
| 1.0.0-alfa-36 | sem sufixo | 2.0.20 | 1.0.25 | 1.9.0 |
| 1.0.0-alfa-35 | sem sufixo | 2.0.20 | 1.0.24 | 1.8.1 |
| 1.0.0-alfa-34 | sem sufixo | 2.0.10 | 1.0.24 | 1.8.1 |
| 1.0.0-alfa-33 | sem sufixo | 2.0.0 | 1.0.24 | 1.8.1 |
| 1.0.0-alfa-30 | sem sufixo | 1.9.24 | 1.0.20 | 1.8.1 |
| 1.0.0-alfa-28 | sem sufixo | 1.9.23 | 1.0.20 | 1.8.0 |
| 1.0.0-alfa-25 | sem sufixo | 1.9.22 | 1.0.17 | 1.8.0 |
| 1.0.0-alfa-23 | sem sufixo | 1.9.21 | 1.0.16 | 1.7.3 |
| 1.0.0-alfa-21 | sem sufixo | 1.9.20 | 1.0.14 | 1.7.3 |
| 1.0.0-alfa-18 | sem sufixo | 1.9.10 | 1.0.13 | 1.7.3 |
| 1.0.0-alfa-17 | sem sufixo | 1.9.0 | 1.0.12 | 1.7.3 |
| 1.0.0-alfa-12 | sem sufixo | 1.8.22 | 1.0.11 | 1.7.2 |
| 1.0.0-alfa-10 | sem sufixo | 1.8.21 | 1.0.11 | 1.7.1 |
| 1.0.0-alfa-7 | sem sufixo | 1.8.20 | 1.0.10 | 1.6.4 |
Você pode escolher entre algumas implementações SWIFT.
Dependendo da implementação, você pode suportar tão baixo quanto iOS 9, MacOS 10.9, TvOS 9 e WatchOS 3:
| Implementação | Swift | iOS | macos | TvOS | vigilância |
|---|---|---|---|---|---|
| Assíncrono | 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 |
A biblioteca consiste em uma parte Kotlin e Swift que você precisará adicionar ao seu projeto.
A parte Kotlin está disponível no Maven Central e a parte Swift pode ser instalada via Cocoapods ou o Swift Package Manager.
Certifique -se de sempre usar as mesmas versões para todas as bibliotecas!
Para Kotlin, basta adicionar o plugin ao seu 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 "
} E certifique -se de optar pela anotação experimental @ObjCName :
kotlin.sourceSets.all {
languageSettings.optIn( " kotlin.experimental.ExperimentalObjCName " )
} As implementações SWIFT estão disponíveis através do Swift Package Manager.
Basta adicioná -lo ao seu arquivo 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 adicione-o no xcode indo ao File > Add Packages... e fornecer o URL: https://github.com/rickclephas/KMP-NativeCoroutines.git .
Observação
A versão para o pacote SWIFT não deve conter o sufixo da versão Kotlin (por exemplo, -new-mm ou -kotlin-1.6.0 ).
Observação
Se você precisar apenas de uma única implementação, também poderá usar as versões específicas do SPM com sufixos -spm-async , -spm-combine e -spm-rxswift .
Se você usa Cocoapods, adicione uma ou mais das seguintes bibliotecas ao seu 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 Observação
A versão para Cocoapods não deve conter o sufixo da versão Kotlin (por exemplo, -new-mm ou -kotlin-1.6.0 ).
Instale o plug -in IDE no mercado da JetBrains para obter:
O uso do código Kotlin Coroutines do Swift é quase tão fácil quanto chamar o código Kotlin.
Basta usar as funções do Wrapper no SWIFT para obter funções assíncronas, assíncadas, editores ou observáveis.
O plug -in gerará automaticamente o código necessário para você! ?
Basta anotar suas declarações de coroutinas com @NativeCoroutines (ou @NativeCoroutinesState ).
Suas propriedades/funções Flow obtêm uma versão 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
}O plug -in gerará esta propriedade nativa para você:
import com.rickclephas.kmp.nativecoroutines.asNativeFlow
import kotlin.native.ObjCName
@ObjCName(name = " time " )
val Clock .timeNative
get() = time.asNativeFlow() Para o StateFlow definido acima, o plug -in também gerará esta propriedade Value:
val Clock .timeValue
get() = time.value No caso de um SharedFlow , o plug -in geraria uma propriedade de cache de reprodução:
val Clock .timeReplayCache
get() = time.replayCache Usando as propriedades StateFlow para rastrear o estado (como em um modelo de exibição)?
Use a anotação @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
}O plug -in gerará essas propriedades nativas para você:
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()O plug -in também gera versões nativas para suas funções de suspensão anotada:
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
}
}O plug -in gerará esta função nativa para você:
import com.rickclephas.kmp.nativecoroutines.nativeSuspend
import kotlin.native.ObjCName
@ObjCName(name = " getRandomLetters " )
fun RandomLettersGenerator. getRandomLettersNative () =
nativeSuspend { getRandomLetters() }Infelizmente, as funções/propriedades de extensão não são suportadas nos protocolos Objective-C.
No entanto, essa limitação pode ser "superada" com alguma mágica rápida.
Supondo que RandomLettersGenerator seja uma interface em vez de uma class podemos fazer o seguinte:
import KMPNativeCoroutinesCore
extension RandomLettersGenerator {
func getRandomLetters ( ) -> NativeSuspend < String , Error , KotlinUnit > {
RandomLettersGeneratorNativeKt . getRandomLetters ( self )
}
} Quando as funções de suspensão e/ou as declarações Flow são expostas ao OBJC/SWIFT, o compilador e o plug-in IDE produzirão um aviso, lembrando que você adiciona uma das anotações KMP-Nativecorines.
Você pode personalizar a gravidade dessas verificações no seu arquivo build.gradle.kts :
nativeCoroutines {
exposedSeverity = ExposedSeverity . ERROR
}Ou, se você não estiver interessado nessas verificações, desative -as:
nativeCoroutines {
exposedSeverity = ExposedSeverity . NONE
} A implementação assíncrona fornece algumas funções para obter funções e AsyncSequence s.
Use a função asyncFunction(for:) Função para obter uma função assíncrona que pode ser aguardada:
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 se você não gosta dessas capturas de doenças, pode usar o asyncResult(for:) função:
import KMPNativeCoroutinesAsync
let result = await asyncResult ( for : randomLettersGenerator . getRandomLetters ( ) )
if case let . success ( letters ) = result {
print ( " Got random letters: ( letters ) " )
} Para funções de retorno Unit , há também o asyncError(for:) função:
import KMPNativeCoroutinesAsync
if let error = await asyncError ( for : integrationTests . returnUnit ( ) ) {
print ( " Failed with error: ( error ) " )
} Para o Flow , existe a asyncSequence(for:) Função para obter uma 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 ( ) A implementação da Combine fornece algumas funções para obter um editor de AnyPublisher código do seu código de coroutinas.
Observação
Essas funções criam AnyPublisher diferidas.
Significando que cada assinatura acionará a coleta do Flow ou execução da função de suspensão.
Observação
Você deve manter uma referência aos Cancellable de retorno, caso contrário, a coleção será cancelada imediatamente.
Para o seu Flow , use o createPublisher(for:) função:
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 ( ) Você também pode usar o createPublisher(for:) Função para suspender funções que retornam um Flow :
let publisher = createPublisher ( for : randomLettersGenerator . getRandomLettersFlow ( ) ) Para as funções de suspensão, você deve usar o createFuture(for:) função:
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 ( ) A implementação do RXSWIFT fornece algumas funções para obter um Observable ou Single para o código do seu Coroutines.
Observação
Essas funções criam Observable e Single S.
Significando que cada assinatura acionará a coleta do Flow ou execução da função de suspensão.
Para o seu Flow , use o createObservable(for:) função:
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 ( ) Você também pode usar o createObservable(for:) Função para suspender funções que retornam um Flow :
let observable = createObservable ( for : randomLettersGenerator . getRandomLettersFlow ( ) ) Para as funções de suspensão, você deve usar o createSingle(for:) função:
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 ( ) Existem várias maneiras pelas quais você pode personalizar o código Kotlin gerado.
Não gosta da nomeação das propriedades/funções geradas?
Especifique seus próprios sufixos personalizados em seu arquivo 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 mais controle, você pode fornecer a um CoroutineScope personalizado com a anotação NativeCoroutineScope :
import com.rickclephas.kmp.nativecoroutines.NativeCoroutineScope
class Clock {
@NativeCoroutineScope
internal val coroutineScope = CoroutineScope (job + Dispatchers . Default )
}Observação
Seu escopo de corotagem personalizado deve ser internal ou public .
Se você não fornecer um CoroutineScope , o escopo padrão será usado, que é definido como:
internal val defaultCoroutineScope = CoroutineScope ( SupervisorJob () + Dispatchers . Default )Observação
O KMP-Nativecoroutines possui suporte interno para o KMP-OBservableViewModel.
Os coroutines dentro do seu ViewModel (por padrão) usarão o CoroutineScope do ViewModelScope .
Use a anotação NativeCoroutinesIgnore para dizer ao plug -in para ignorar uma propriedade ou função:
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore
@NativeCoroutinesIgnore
val ignoredFlowProperty : Flow < Int >
@NativeCoroutinesIgnore
suspend fun ignoredSuspendFunction () { } Se, por algum motivo, você gostaria de refinar ainda mais suas declarações de Kotlin no Swift, poderá usar as anotações NativeCoroutinesRefined e NativeCoroutinesRefinedState .
Isso dirá ao plug -in que adicione a anotação ShouldRefineInSwift às propriedades/função gerada.
Observação
Atualmente, isso requer uma opção de opção em todo o módulo para kotlin.experimental.ExperimentalObjCRefinement .
Você pode, por exemplo, refinar sua propriedade Flow para uma propriedade 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 )
}
}