rickclephas / KMP-NativeCoroutines

Library to use Kotlin Coroutines from Swift code in KMP apps
MIT License
1.03k stars 31 forks source link

Issue for normal function that returns Flow #103

Closed zhuyifan2013 closed 1 year ago

zhuyifan2013 commented 1 year ago

After upgrading to 1.x from 0.x, some functions could not work in Swift:

Codes like this, do I need to add @NativeCoroutines to it ? If I added, I could not find this method in Swift, if not, this also could be called properly.

fun login(loginRequest: LoginRequest): Flow<APIResult> 
zhuyifan2013 commented 1 year ago

I founded the generated codes for this :

@ObjCName(name = "login")
public fun IUserRepository.loginNative(loginRequest: IUserRepository.LoginRequest):
    NativeFlow<APIResult> = login(loginRequest).asNativeFlow(null)

But seems this generated file is not founded by Swift

zhuyifan2013 commented 1 year ago
CleanShot 2023-04-09 at 17 15 46@2x
zhuyifan2013 commented 1 year ago

This is my shared setting:

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    kotlin("plugin.serialization") version "1.8.10"
    id("com.google.devtools.ksp") version "1.8.20-1.0.10" apply (true)
    id("com.rickclephas.kmp.nativecoroutines") version "1.0.0-ALPHA-6"
    id("com.android.library")
}

val packageName = "com.yifan.fallinlove"
val ktorVersion = "2.1.3"
val ktCoroutineVersion = "1.6.4"
val apolloVersion = "3.6.2"
val koinVersion = "3.2.2"
val koinKtor = "3.2.2"
//val koinKspVersion = "1.0.3"
val sqlDelightVersion = "1.5.3"
//val kspCompiler = "io.insert-koin:koin-ksp-compiler:${koinKspVersion}"
val napierVersion = "2.6.1"

kotlin {
    android()
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    cocoapods {
        summary = "Some description for the Shared Module"
        homepage = "Link to the Shared Module homepage"
        version = "0.1.1"
        ios.deploymentTarget = "14.1"
        podfile = project.file("../iosApp/Podfile")
        framework {
            baseName = "shared"
            isStatic = false
        }
    }

    sourceSets {
        all {
            languageSettings.optIn("kotlin.RequiresOptIn")
            languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")
        }

        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$ktCoroutineVersion")
                implementation("io.ktor:ktor-client-core:$ktorVersion")
                implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
                implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
                implementation("io.insert-koin:koin-core:$koinVersion")
                implementation(libs.kotlinx.serialization.properties)
                implementation(libs.androidx.dataStore.preferences.core)
                implementation("io.github.aakira:napier:$napierVersion")
//                implementation("com.google.devtools.ksp:symbol-processing-api:1.8.20-1.0.10")
                api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
                api("androidx.datastore:datastore-core-okio:1.1.0-dev01")
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-android:$ktorVersion")
            }
        }
        val androidTest by getting
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting
        val iosMain by creating {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            iosSimulatorArm64Main.dependsOn(this)

            dependencies {
                implementation("io.ktor:ktor-client-darwin:$ktorVersion")
            }
        }
        val iosX64Test by getting
        val iosArm64Test by getting
        val iosSimulatorArm64Test by getting
        val iosTest by creating {
            dependsOn(commonTest)
            iosX64Test.dependsOn(this)
            iosArm64Test.dependsOn(this)
            iosSimulatorArm64Test.dependsOn(this)
        }
    }
}

android {
    namespace = packageName
    compileSdk = 33
    defaultConfig {
        minSdk = 29
        targetSdk = 33
    }
}

//dependencies {
//    add("kspCommonMainMetadata", kspCompiler)
//    add("kspAndroid", kspCompiler)
//    add("kspIosX64", kspCompiler)
//    add("kspIosSimulatorArm64", kspCompiler)
//}

kotlin.targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java) {
    binaries.all {
        binaryOptions["memoryModel"] = "experimental"
    }
}
rickclephas commented 1 year ago

Hi! I am assuming that IUserRepository is an interface, right? In that case you are seeing a limitation of the Objective-C interop, where extensions on interfaces/protocols aren't supported. Such extensions are exposed on a Kt object instead. Assuming your file is also called IUserRepository you should be able to access the extension function with:

let repo: IUserRepository
let loginRequest: IUserRepository.LoginRequest
let nativeFlow = IUserRepositoryNativeKt.login(repo, loginRequest: loginRequest)

To make this easier to work with you could define an extension in Swift:

extension IUserRepository {
    func login(loginRequest: IUserRepository.LoginRequest) -> AnyPublisher<APIResult, Error> {
        return createPublisher(for: IUserRepositoryNativeKt.login(self, loginRequest: loginRequest))
    }
}

In this case I used the createPublisher(for:) function from the Combine implementation, but you could obviously use any Swift implementation you like.

I will take a look at the documentation to make sure these limitations are better documented.

hosseinaminii commented 1 year ago

@rickclephas I have the same issue in version 1.0.0-ALPHA-4 When I annotate a stateflow with @NativeCoroutinesState it won't be visible on the iOS side anymore.
I'm using it in my shared view models which are classes, not interfaces.

rickclephas commented 1 year ago

@hosseinaminii could you possibly share some of the affected code? Are you using multiple Kotlin modules or just a single shared module?

hosseinaminii commented 1 year ago

@rickclephas The project has 3 KMM modules which are data, domain, and shared(The shared module has access to the data and domain modules). We use the shared module to share view models which iOS and Android have access to it and I added this library to this module. Also, I added the KMPNativeCoroutinesAsync library to the pods and installed it. Everything was straightforward and there was no problem in adding the library to the project but unfortunately, it has the mentioned issue.

zhuyifan2013 commented 1 year ago

Hi @rickclephas , thanks for figuring out, but there is still a problem that confuses me, what is the return type for this ?

CleanShot 2023-04-10 at 15 31 30@2x

The generated code is :

@ObjCName(name = "login")
public fun IUserRepository.loginNative(loginRequest: IUserRepository.LoginRequest):
    NativeFlow<APIResult> = login(loginRequest).asNativeFlow(null)

And the original interface definition is :

    @NativeCoroutines
    fun login(loginRequest: LoginRequest): Flow<APIResult>
rickclephas commented 1 year ago

@hosseinaminii alright that sound like a setup that should be supported. Could you possibly share a minimum reproducible sample?

rickclephas commented 1 year ago

@zhuyifan2013 in that case your login function should have the following return type: NativeFlowAsyncSequence< APIResult, Error, KotlinUnit>.

hosseinaminii commented 1 year ago

@rickclephas Ok, I'll try to create a sample project as soon as possible and share it with you. Thank you

zhuyifan2013 commented 1 year ago

@rickclephas Yes, it works, thanks very much!!

hhpettersen commented 11 months ago

@rickclephas The project has 3 KMM modules which are data, domain, and shared(The shared module has access to the data and domain modules). We use the shared module to share view models which iOS and Android have access to it and I added this library to this module. Also, I added the KMPNativeCoroutinesAsync library to the pods and installed it. Everything was straightforward and there was no problem in adding the library to the project but unfortunately, it has the mentioned issue.

@hosseinaminii Can't see that you got a suggestion on how to resolve this issue? Previously we could access state of StateFlow by a generated value called stateNative, but it is no longer generated. How did you resolve this?

rickclephas commented 11 months ago

@hhpettersen in the 1.0 releases the native suffix has been removed. So a StateFlow property called state would have a stateValue (or state when using the @NativeCoroutinesState annotation) property that exposes the value of the StateFlow.