Skip to content

Repository files navigation

Maven Central

Koru

Automatically generates wrappers for suspend functions and Flow for easy access from Swift code in Kotlin Multiplatform projects.

Inspired by https://touchlab.co/kotlin-coroutines-rxswift/ by Russell Wolf.

Note: version 0.11.0 introduces KSP support. Support for kapt will still be provided for some time.

Getting started

To get started, consult the Basic example below, read introductory article or check out the example repo.

Basic example

Let's say you have a class in the shared module, that looks like this:

@ToNativeClass(name ="LoadUserUseCaseIos")
classLoadUserUseCase(privatevalservice:Service) {
suspendfunloadUser(username:String) : User?= service.loadUser(username)
}

Such use case can be easily consumed from Android code, but in Kotlin Native (e.g. iOS) suspend functions generate a completion handler which is a bit of a PITA to work with.

When you add @ToNativeClass annotation to the class, a wrapper is generated:

publicclassLoadUserUseCaseIos(privatevalwrapped:LoadUserUseCase) {
publicfunloadUser(username:String): SuspendWrapper<User?> =SuspendWrapper(null) { wrapped.loadUser(username) }
}

Notice that in place of suspend function, we get a function exposing SuspendWrapper. When you expose LoadUserUseCaseIos to your Swift code, it can be consumed like this:

loadUserUseCaseIos.loadUser(username:"foo").subscribe(
scope: coroutineScope, //this can be provided automatically, more on that below
onSuccess:{ user inprint(user?.description()??"none")},
onThrow:{ error inprint(error.description())})

From here it can be easily wrapped into RxSwift Single<User?> or Combine AnyPublisher<User?, Error>.

Generated functions / properties - Suspend, Flow and regular

The wrappers generate different return types based on the original member signature

OriginalWrapper
suspend fun returning Tfun returning SuspendWrapper<T>
fun returning Flow<T>fun returning FlowWrapper<T>
fun returning Tfun returning T
val / var returning Flow<T>val returning FlowWrapper<T>
val / var returning Tval returning T

So, for example, this class:

@ToNativeClass(name ="LoadUserUseCaseIos")
classLoadUserUseCase(privatevalservice:Service) {
suspendfunloadUser(username:String) : User?= service.loadUser(username)
funobserveUser(username:String) : Flow<User?> = service.observeUser(username)
fungetUser(username:String) : User?= service.getUser(username)
val someone :User? get() = service.getUser("someone")
val someoneFlow :Flow<User> = service.observeUser("someone")
}

becomes:

publicclassLoadUserUseCaseIos(privatevalwrapped:LoadUserUseCase) {
publicfunloadUser(username:String): SuspendWrapper<User?> =SuspendWrapper(null) { wrapped.loadUser(username) }
publicfunobserveUser(username:String): FlowWrapper<User?> =FlowWrapper(null, wrapped.observeUser(username))
publicfungetUser(username:String): User?= wrapped.getUser(username)
publicval someone:User?
get() = wrapped.someone
publicval someoneFlow:FlowWrapper<User>
get() = com.futuremind.koru.FlowWrapper(null, wrapped.someoneFlow)
}

More options

Customizing generated names

You can control the name of the generated class or interface:

  • @ToNativeClass(name = "MyFancyIosClass")
  • @ToNativeInterface(name = "MyFancyIosProtocol")

You can also omit the name parameter and use the defaults:

  • @ToNativeClass Foo becomes FooNative
  • @ToNativeInterface Foo becomes FooNativeProtocol

Provide the scope automatically

One of the caveats of accessing suspend functions / Flows from Swift code is that you still have to provide CoroutineScope from the Swift code. This might upset your iOS team ;). In the spirit of keeping the shared code API as business-focused as possible, we can utilize @ExportScopeProvider to handle scopes automagically.

First you need to show the suspend wrappers where to look for the scope, like this:

@ExportedScopeProvider
classMainScopeProvider : ScopeProvider {
overrideval scope =MainScope()
}

And then you provide the scope like this

@ToNativeClass(launchOnScope =MainScopeProvider::class)

Thanks to this, your Swift code can be simplified to just the callbacks, scope that launches coroutines is handled implicitly.

loadUserUseCaseIos.loadUser(username:"some username").subscribe(
onSuccess:{ user inprint(user?.description()??"none")},
onThrow:{ error inprint(error.description())})
What happens under the hood?

Under the hood, a top level property val exportedScopeProvider_mainScopeProvider = MainScopeProvider() is created. Then, it is injected into the constructor of the wrapped class and then into SuspendWrappers and FlowWrappers as the default scope that launches the coroutines. Remember, that you can always override with your custom scope if you need to.

publicclassLoadUserUseCaseIos(
privatevalwrapped:LoadUserUseCase,
privatevalscopeProvider:ScopeProvider?
) {
funflow(foo:String) =FlowWrapper(scopeProvider, wrapped.flow(foo))
funsuspending(foo:String) =SuspendWrapper(scopeProvider) { wrapped.suspending(foo) }
}

Generate interfaces from classes and classes from interfaces

Usually you will just need to use @ToNativeClass on your business logic class like in the basic example. However, you can get more fancy, if you want.

Generate interface from class

Say, you want to expose to Swift code both the class and an interface (which translates to protocol in Swift), so that you can use the protocol to create a fake impl for unit tests.

@ToNativeClass(name ="FooIos")
@ToNativeInterface(name ="FooIosProtocol")
classFoo

This code will create an interface and a class extending it.

interfaceFooIosProtocolclassFooIos(privatevalwrapped:Foo) : FooIosProtocol

Generate interface from interface

If you already have an interface, you can reuse it just as easily:

@ToNativeInterface(name ="FooIosProtocol")
interfaceIFoo
@ToNativeClass(name ="FooIos")
classFoo : IFoo

This will also create an interface and a class and automatically match them:

interfaceFooIosProtocolclassFooIos(privatevalwrapped:Foo) : FooIosProtocol

Generate class from interface

*Not sure what the use case might be, nevertheless, it's also possible:

@ToNativeClass(name ="FooIos")
interfaceFoo

Will generate:

classFooIos(privatevalwrapped:Foo)

Handling in Swift code

You can consume the coroutine wrappers directly as callbacks. But if you are working with Swift Combine, you can wrap those callbacks using simple global functions (extension functions are not supported for Kotlin Native generic types at this time).

Then, you can call them like this:

createPublisher(wrapper: loadUserUseCase.loadUser(username:"Bob")).sink(
receiveCompletion:{ completion inprint("Completion: \(completion)")},
receiveValue:{ user inprint("Hello from the Kotlin side \(user?.name)")}).store(in:&cancellables)

Similar helper functions can be easily created for RxSwift.

Download

The artifacts are available on Maven Central and the compiler plugin in Gradle Plugin Portal.

To use the library in a KMM project, use this config in the build.gradle.kts:

plugins {
//add ksp and koru compiler plugin
id("com.google.devtools.ksp") version "1.6.21-1.0.6"
id("com.futuremind.koru").version("0.11.1")
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
// add library dependency
implementation("com.futuremind:koru:0.11.1")
}
}
val iosMain by creating {
...
}
}
}
koru {
// let the compiler plugin know where the generated code should be available// by providing the name of ios source set
nativeSourceSetNames =listOf("iosMain")
}
Legacy kapt support

Starting from version 0.11.0 this library supports ksp which is the recommended way. kapt is still available, though, with the following configuration.

plugins {
kotlin("multiplatform")
kotlin("kapt")
...
}
kotlin {
...
sourceSets {
...
val commonMain by getting {
dependencies {
...
implementation("com.futuremind:koru:0.12.0")
configurations.get("kapt").dependencies.add(
org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency(
"com.futuremind", "koru-processor", "0.12.0"
)
)
}
}
val iosMain by getting {
...
kotlin.srcDir("${buildDir.absolutePath}/generated/source/kaptKotlin/")
}
}
}

Compatibility

KoruKSPKotlin
0.11.11.6.21-1.0.x
1.7.0-1.0.x
1.7.10-1.0.x
1.6.21
1.7.0
1.7.10
0.12.01.6.21-1.0.x
1.7.0-1.0.x
1.7.10-1.0.x
1.8.0-1.0.x
1.6.21
1.7.0
1.7.10
1.8.0

This library should be compatible with any version of coroutines.

If you find any compatibility issues, let us know.

About

Simple coroutine wrappers for Kotlin Native. Generated from annotations. Compatible with RxSwift, Combine, async-await.

Topics

Resources

Stars

216 stars

Watchers

7 watching

Forks

Used by

Contributors

Languages