Giter VIP home page Giter VIP logo

koru's Introduction

This project is forked and modified from koru.

What is modified?

  • Rewrite with ksp.
  • Generate extension functions instead of wrapper classes.
  • Changed package name and artifact group to avoid conflicts with the original library.
  • This README.md is modified to reflect usage of this modified project.

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: this library is in beta state - the API should be mostly stable but there might be minor changes.

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
class LoadUserUseCase(private val service: Service) {

    suspend fun loadUser(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 file is generated:

public fun LoadUserUseCase.loadUserNative(username: String): SuspendWrapper<User?> = 
  SuspendWrapper(null) { this.loadUser(username) }
  

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

loadUserUseCase.loadUserNative(username: "foo").subscribe(
            scope: coroutineScope, //this can be provided automatically, more on that below
            onSuccess: { user in print(user?.description() ?? "none") },
            onThrow: { error in print(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

Original Wrapper
suspend fun returning T fun returning SuspendWrapper<T>
fun returning Flow<T> fun returning FlowWrapper<T>
fun returning T Nop
val / var returning Flow<T> val returning FlowWrapper<T>
val / var returning T Nop

So, for example, this class:

@ToNativeClass
class LoadUserUseCase(private val service: Service) {

    suspend fun loadUser(username: String) : User? = service.loadUser(username)
    
    fun observeUser(username: String) : Flow<User?> = service.observeUser(username)
    
    fun getUser(username: String) : User? = service.getUser(username)

    val someone : User? get() = service.getUser("someone")

    val someoneFlow : Flow<User> = service.observeUser("someone")

}

becomes:

public fun LoadUserUseCase.loadUser(username: String): SuspendWrapper<User?> =
    SuspendWrapper(null) { this.loadUser(username) }

public fun LoadUserUseCase.observeUser(username: String): FlowWrapper<User?> =
    FlowWrapper(null, this.observeUser(username))

public val LoadUserUseCase.someoneFlow: FlowWrapper<User>
    get() = com.futuremind.koru.FlowWrapper(null, this.someoneFlow)
    

More options

Customizing generated names

This is not supported.

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
class MainScopeProvider : ScopeProvider {

    override val 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.

loadUserUseCase.loadUserNative(username: "some username").subscribe(
            onSuccess: { user in print(user?.description() ?? "none") },
            onThrow: { error in print(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 generated file 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.

private val scopeProvider: ScopeProvider?

fun LoadUserUseCaseIos.flow(foo: String) = FlowWrapper(scopeProvider, wrapped.flow(foo))
fun LoadUserUseCaseIos.suspending(foo: String) = SuspendWrapper(scopeProvider) { wrapped.suspending(foo) }

Generate interfaces from classes and classes from interfaces

This part is not applicable. We only generate extension functions.

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.loadUserNative(username: "Bob"))
    .sink(
        receiveCompletion: { completion in print("Completion: \(completion)") },
        receiveValue: { user in print("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.

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

plugins {
    kotlin("multiplatform")
    id("com.google.devtools.ksp") version "1.6.10-1.0.2"
    ...
}

kotlin {

  ...
  
  sourceSets {
        
        ...
  
        val commonMain by getting {
            dependencies {
                ...
                implementation("com.futuremind.koruksp:koruksp:0.11.0")

            }
        }
        
        val iosMain by getting {
            ...
            kotlin.srcDir("${buildDir.absolutePath}/generated/source/kaptKotlin/")
        }
        
    }
    
}

dependencies {
    val koruKspProcessor = "com.futuremind.koruksp:koruksp-processor:0.11.0"
    add("kspIosArm64", koruKspProcessor)
    add("kspIosX64", koruKspProcessor)
    // ... Other ios platforms
}

koru's People

Contributors

michar avatar takahirom avatar johnan avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.