{"id":15038598,"url":"https://github.com/futuremind/koru","last_synced_at":"2025-04-10T04:59:30.241Z","repository":{"id":42432570,"uuid":"332756025","full_name":"FutureMind/koru","owner":"FutureMind","description":"Simple coroutine wrappers for Kotlin Native. Generated from annotations. Compatible with RxSwift, Combine, async-await.","archived":false,"fork":false,"pushed_at":"2023-08-16T02:07:58.000Z","size":228,"stargazers_count":212,"open_issues_count":5,"forks_count":17,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-04-02T03:34:42.848Z","etag":null,"topics":["coroutines","flow","kmm","kmp","kotlin-coroutines","kotlin-multiplatform","kotlin-native","suspend","swift"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/FutureMind.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-01-25T13:27:48.000Z","updated_at":"2024-11-10T08:41:57.000Z","dependencies_parsed_at":"2024-01-16T15:40:25.646Z","dependency_job_id":"1fe69103-58e6-4749-8af6-c28de426ab2f","html_url":"https://github.com/FutureMind/koru","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FutureMind%2Fkoru","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FutureMind%2Fkoru/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FutureMind%2Fkoru/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FutureMind%2Fkoru/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/FutureMind","download_url":"https://codeload.github.com/FutureMind/koru/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248161266,"owners_count":21057554,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coroutines","flow","kmm","kmp","kotlin-coroutines","kotlin-multiplatform","kotlin-native","suspend","swift"],"created_at":"2024-09-24T20:39:08.882Z","updated_at":"2025-04-10T04:59:30.209Z","avatar_url":"https://github.com/FutureMind.png","language":"Kotlin","readme":"[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.futuremind/koru/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.futuremind/koru)\n\n# Koru\n\nAutomatically generates wrappers for `suspend` functions and `Flow` for easy access from Swift code in Kotlin Multiplatform projects.\n\nInspired by https://touchlab.co/kotlin-coroutines-rxswift/ by Russell Wolf.\n\n**Note**: version 0.11.0 introduces [KSP](https://kotlinlang.org/docs/ksp-overview.html) support. Support for kapt will still be provided for some time.\n\n## Getting started\n\nTo get started, consult the Basic example below, read [introductory article](https://medium.com/futuremind/handling-kotlin-multiplatform-coroutines-in-swift-koru-4a80b93f232b) or check out the [example repo](https://github.com/FutureMind/koru-example).\n\n### Basic example\n\nLet's say you have a class in the `shared` module, that looks like this:\n\n```kotlin\n@ToNativeClass(name = \"LoadUserUseCaseIos\")\nclass LoadUserUseCase(private val service: Service) {\n\n    suspend fun loadUser(username: String) : User? = service.loadUser(username)\n    \n}\n```\n\nSuch 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.\n\nWhen you add `@ToNativeClass` annotation to the class, a wrapper is generated:\n\n```kotlin\npublic class LoadUserUseCaseIos(private val wrapped: LoadUserUseCase) {\n\n  public fun loadUser(username: String): SuspendWrapper\u003cUser?\u003e = \n      SuspendWrapper(null) { wrapped.loadUser(username) }\n  \n}\n```\n\nNotice 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:\n\n```swift\nloadUserUseCaseIos.loadUser(username: \"foo\").subscribe(\n            scope: coroutineScope, //this can be provided automatically, more on that below\n            onSuccess: { user in print(user?.description() ?? \"none\") },\n            onThrow: { error in print(error.description())}\n        )\n```\n\nFrom here it can be easily wrapped into RxSwift `Single\u003cUser?\u003e` or Combine `AnyPublisher\u003cUser?, Error\u003e`.\n\n## Generated functions / properties - Suspend, Flow and regular\n\nThe wrappers generate different return types based on the original member signature\n\n| Original | Wrapper |\n|-|-|\n| `suspend` fun returning `T` | fun returning `SuspendWrapper\u003cT\u003e` |\n| fun returning `Flow\u003cT\u003e` | fun returning `FlowWrapper\u003cT\u003e` |\n| fun returning `T` | fun returning `T` |\n| val / var returning `Flow\u003cT\u003e` | val returning `FlowWrapper\u003cT\u003e` |\n| val / var returning `T` | val returning `T` |\n\nSo, for example, this class:\n\n```kotlin\n@ToNativeClass(name = \"LoadUserUseCaseIos\")\nclass LoadUserUseCase(private val service: Service) {\n\n    suspend fun loadUser(username: String) : User? = service.loadUser(username)\n    \n    fun observeUser(username: String) : Flow\u003cUser?\u003e = service.observeUser(username)\n    \n    fun getUser(username: String) : User? = service.getUser(username)\n\n    val someone : User? get() = service.getUser(\"someone\")\n\n    val someoneFlow : Flow\u003cUser\u003e = service.observeUser(\"someone\")\n\n}\n```\n\nbecomes:\n\n```kotlin\npublic class LoadUserUseCaseIos(private val wrapped: LoadUserUseCase) {\n\n    public fun loadUser(username: String): SuspendWrapper\u003cUser?\u003e =\n        SuspendWrapper(null) { wrapped.loadUser(username) }\n\n    public fun observeUser(username: String): FlowWrapper\u003cUser?\u003e =\n        FlowWrapper(null, wrapped.observeUser(username))\n        \n    public fun getUser(username: String): User? = wrapped.getUser(username)\n\n    public val someone: User?\n        get() =  wrapped.someone\n\n    public val someoneFlow: FlowWrapper\u003cUser\u003e\n        get() = com.futuremind.koru.FlowWrapper(null, wrapped.someoneFlow)\n    \n}\n```\n\n## More options\n\n### Customizing generated names\n\nYou can control the name of the generated class or interface:\n- `@ToNativeClass(name = \"MyFancyIosClass\")`\n- `@ToNativeInterface(name = \"MyFancyIosProtocol\")`\n\nYou can also omit the `name` parameter and use the defaults:\n- `@ToNativeClass Foo` becomes `FooNative`\n- `@ToNativeInterface Foo` becomes `FooNativeProtocol`\n\n### Provide the scope automatically\n\nOne 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.\n\nFirst you need to show the suspend wrappers where to look for the scope, like this:\n\n```kotlin\n@ExportedScopeProvider\nclass MainScopeProvider : ScopeProvider {\n\n    override val scope = MainScope()\n    \n}\n```\n\nAnd then you provide the scope like this\n\n```kotlin\n@ToNativeClass(launchOnScope = MainScopeProvider::class)\n```\n\nThanks to this, your Swift code can be simplified to just the callbacks, scope that launches coroutines is handled implicitly.\n\n```swift\nloadUserUseCaseIos.loadUser(username: \"some username\").subscribe(\n            onSuccess: { user in print(user?.description() ?? \"none\") },\n            onThrow: { error in print(error.description())}\n        )\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eWhat happens under the hood?\u003c/summary\u003e\n    \n  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 `SuspendWrapper`s and `FlowWrapper`s as the default scope that `launch`es the coroutines. Remember, that you can always override with your custom scope if you need to.\n  \n  ```kotlin\n    public class LoadUserUseCaseIos(\n      private val wrapped: LoadUserUseCase,\n      private val scopeProvider: ScopeProvider?\n    ) {\n      fun flow(foo: String) = FlowWrapper(scopeProvider, wrapped.flow(foo))\n      fun suspending(foo: String) = SuspendWrapper(scopeProvider) { wrapped.suspending(foo) }\n    }\n  ```\n\n\u003c/details\u003e\n\n### Generate interfaces from classes and classes from interfaces\n\nUsually 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. \n\n#### Generate interface from class\n\nSay, 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.\n\n```kotlin\n@ToNativeClass(name = \"FooIos\")\n@ToNativeInterface(name = \"FooIosProtocol\")\nclass Foo\n```\n\nThis code will create an interface and a class extending it.\n\n```kotlin\ninterface FooIosProtocol\n\nclass FooIos(private val wrapped: Foo) : FooIosProtocol\n```\n\n#### Generate interface from interface\n\nIf you already have an interface, you can reuse it just as easily:\n\n```kotlin\n@ToNativeInterface(name = \"FooIosProtocol\")\ninterface IFoo\n\n@ToNativeClass(name = \"FooIos\")\nclass Foo : IFoo\n```\n\nThis will also create an interface and a class and automatically match them:\n\n```kotlin\ninterface FooIosProtocol\n\nclass FooIos(private val wrapped: Foo) : FooIosProtocol\n```\n\n#### Generate class from interface\n\n*Not sure what the use case might be, nevertheless, it's also possible:\n\n```kotlin\n@ToNativeClass(name = \"FooIos\")\ninterface Foo\n```\n\nWill generate:\n\n```kotlin\nclass FooIos(private val wrapped: Foo)\n```\n\n## Handling in Swift code\n\nYou 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](https://github.com/FutureMind/koru-example/blob/master/iosApp/iosApp/Utils/Coroutine2Combine.swift) (extension functions are not supported for Kotlin Native generic types at this time).\n\nThen, you can call them like this:\n\n```swift\ncreatePublisher(wrapper: loadUserUseCase.loadUser(username: \"Bob\"))\n    .sink(\n        receiveCompletion: { completion in print(\"Completion: \\(completion)\") },\n        receiveValue: { user in print(\"Hello from the Kotlin side \\(user?.name)\") }\n    )\n    .store(in: \u0026cancellables)\n```\n\nSimilar helper functions can be easily created for RxSwift.\n\n## Download\n\nThe artifacts are available on Maven Central and the compiler plugin in Gradle Plugin Portal. \n\nTo use the library in a KMM project, use this config in the `build.gradle.kts`:\n\n```kotlin\nplugins {\n    //add ksp and koru compiler plugin\n    id(\"com.google.devtools.ksp\") version \"1.6.21-1.0.6\"\n    id(\"com.futuremind.koru\").version(\"0.11.1\")\n}\n\nkotlin {\n  \n  sourceSets {\n  \n        val commonMain by getting {\n            dependencies {\n                // add library dependency\n                implementation(\"com.futuremind:koru:0.11.1\")\n            }\n        }\n      \n        val iosMain by creating {\n            ...\n        }\n        \n    }\n    \n}\n\nkoru {\n    // let the compiler plugin know where the generated code should be available\n    // by providing the name of ios source set\n    nativeSourceSetNames = listOf(\"iosMain\")\n}\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eLegacy kapt support\u003c/summary\u003e\n\nStarting from version 0.11.0 this library supports `ksp` which is the recommended way. `kapt` is\nstill available, though, with the following configuration.\n\n```kotlin\nplugins {\n    kotlin(\"multiplatform\")\n    kotlin(\"kapt\")\n    ...\n}\n\nkotlin {\n\n  ...\n  \n  sourceSets {\n        \n        ...\n  \n        val commonMain by getting {\n            dependencies {\n                ...\n                implementation(\"com.futuremind:koru:0.12.0\")\n                configurations.get(\"kapt\").dependencies.add(\n                    org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency(\n                        \"com.futuremind\", \"koru-processor\", \"0.12.0\"\n                    )\n                )\n\n            }\n        }\n        \n        val iosMain by getting {\n            ...\n            kotlin.srcDir(\"${buildDir.absolutePath}/generated/source/kaptKotlin/\")\n        }\n        \n    }\n    \n}\n```\n\n\u003c/details\u003e\n\n### Compatibility\n\n| Koru   | KSP                                                               | Kotlin                                    |\n|--------|-------------------------------------------------------------------|-------------------------------------------|\n| 0.11.1 | 1.6.21-1.0.x\u003cbr\u003e 1.7.0-1.0.x\u003cbr\u003e 1.7.10-1.0.x\u003cbr\u003e                 | 1.6.21\u003cbr\u003e 1.7.0\u003cbr\u003e 1.7.10\u003cbr\u003e           |\n| 0.12.0 | 1.6.21-1.0.x\u003cbr\u003e 1.7.0-1.0.x\u003cbr\u003e 1.7.10-1.0.x\u003cbr\u003e 1.8.0-1.0.x\u003cbr\u003e | 1.6.21\u003cbr\u003e 1.7.0\u003cbr\u003e 1.7.10\u003cbr\u003e 1.8.0\u003cbr\u003e |\n\nThis library should be compatible with any version of coroutines.\n\nIf you find any compatibility issues, let us know.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffuturemind%2Fkoru","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffuturemind%2Fkoru","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffuturemind%2Fkoru/lists"}