rickclephas / KMP-NativeCoroutines

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

All Properties in Swift are "any Kotlinx_coroutines_coreMutableStateFlow" #193

Closed LivPNavusoft closed 1 month ago

LivPNavusoft commented 1 month ago

I've been seeing a bit of an odd issue that I couldn't find being addressed elsewhere. I was able to create viewModels that work as expected in Android, and have @NativeCoroutinesState etc to map them to Swift. However, regardless of the variable's type, it's ported over as an "any".

For example, the following property in my kotlin common code's viewmodel @NativeCoroutinesState var dummyVar = MutableStateFlow<String?>(viewModelScope, null)

Is generated in Swift as @property id<KmpSharedKotlinx_coroutines_coreMutableStateFlow> dummyVar __attribute__((swift_name("dummyVar")));

Subsequently, I'm unable to use them in TextFields etc in Swift. This is the case for any type, nullable or otherwise. I was originally having this issue with the viewmodels before adding KMP-NativeCoroutines, but adding the library to Kotlin & Swift and adding the expected annotations doesn't seem to have had an effect.

Apologies if I'm missing something, but I greatly appreciate any assistance! Below is some versioning I thought may be relevant

rickclephas commented 1 month ago

Hmm, that's quite interesting. The KMP-NativeCoroutines annotations should actually completely hide the original declaration from Swift (even if the plugin itself isn't fully configured).

Could you share some more information about your project setup? E.g. is it just a single shared module or multiple shared Kotlin modules? How is KMP-NativeCoroutines configured in the module(s)?

LivPNavusoft commented 1 month ago

Thank you for getting back to me so quickly! I'll answer your questions first, and then I have an update that may help others...and another related odd problem, lol

We have a single shared kotlin module, with ios and android sourceSets. We use SwiftUI and androidx.Navigation/fragments and activities with traditional views. We use Kodein, so the viewmodels extend DIAware so that we can inject repositories and APIs - removing the DIAware and dependency injections don't resolve the issue and DI works as expected in Android, but I thought it may be relevant.

For NativeCoroutines, in our project build.gradle we preload our plugins for performance and use

plugins {
  ...
  alias(libs.plugins.nativeCoroutines).apply(false)
}

In our shared gradle to actually apply the plugin, we have

  plugins{
    ...
   alias(libs.plugins.nativeCoroutines)
}

kotlin{
  ...
  sourceSets {
    all {
            languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi")
            languageSettings.optIn("kotlin.experimental.ExperimentalObjCName")
        }
  }
}

In XCode in the podfile, we use

pod 'KMPNativeCoroutinesAsync', '1.0.0-ALPHA-36'    # Swift Concurrency implementation
pod 'KMPNativeCoroutinesCombine', '1.0.0-ALPHA-36'  # Combine implementation
pod 'KMPNativeCoroutinesRxSwift', '1.0.0-ALPHA-36'  # RxSwift implementation

Regarding the original issue, I thought perhaps part of the build wasn't actually running as expected, so I invalidated caches/cleaned the project/rebuilt. This exposed a Moko Resource related issue we weren't seeing previously; I'm unsure if it's a conflict issue as Moko uses ksp as well, or if it's an existing issue that came up during troubleshooting. For anyone else seeing the following error

Reason: Task ':shared:kspKotlinIosSimulatorArm64' uses this output of task ':shared:generateMRiosSimulatorArm64Main' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.

The fix is adding the following to the shared build.gradle

project.afterEvaluate {
    tasks.named("kspKotlinIosSimulatorArm64") {
        dependsOn("generateMRiosSimulatorArm64Main")
    }
    tasks.named("kspKotlinIosX64") {
        dependsOn("generateMRiosX64Main")
    }
    tasks.named("kspKotlinIosArm64") {
        dependsOn("generateMRiosArm64Main")
    }
}

This also appeared to fix, or at least change, the original issue. Now the properties are generated in Swift as

  @property (readonly) KmpSharedKotlinUnit *(^(^dummyVarFlow)(KmpSharedKotlinUnit *(^)(NSString * _Nullable, KmpSharedKotlinUnit *(^)(void), KmpSharedKotlinUnit *), KmpSharedKotlinUnit *(^)(NSError * _Nullable, KmpSharedKotlinUnit *), KmpSharedKotlinUnit *(^)(NSError *, KmpSharedKotlinUnit *)))(void) __attribute__((swift_name("dummyVarFlow")));
@property NSString * _Nullable dummyVar __attribute__((swift_name("dummyVar")));

While this seems to be an improvement, issues such as https://github.com/rickclephas/KMP-ObservableViewModel/issues/31 seem to indicate that Bindings should be generated instead of the underlying type variables - in this case String?. I'm using both @NativeCoroutinesState for the properties, as well as @StateViewModel for vm. Is this expected, with manual bindings needing to be used instead?

rickclephas commented 1 month ago

Regarding the original issue, I thought perhaps part of the build wasn't actually running as expected, so I invalidated caches/cleaned the project/rebuilt. This exposed a Moko Resource related issue we weren't seeing previously; I'm unsure if it's a conflict issue as Moko uses ksp as well, or if it's an existing issue that came up during troubleshooting. For anyone else seeing the following error

Reason: Task ':shared:kspKotlinIosSimulatorArm64' uses this output of task ':shared:generateMRiosSimulatorArm64Main' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.

That seems to be a known issue with MOKO resources: https://github.com/icerockdev/moko-resources/issues/514.

This also appeared to fix, or at least change, the original issue. Now the properties are generated in Swift as

@property (readonly) KmpSharedKotlinUnit *(^(^dummyVarFlow)(KmpSharedKotlinUnit *(^)(NSString * _Nullable, KmpSharedKotlinUnit *(^)(void), KmpSharedKotlinUnit *), KmpSharedKotlinUnit *(^)(NSError * _Nullable, KmpSharedKotlinUnit *), KmpSharedKotlinUnit *(^)(NSError *, KmpSharedKotlinUnit *)))(void) __attribute__((swift_name("dummyVarFlow")));
@property NSString * _Nullable dummyVar __attribute__((swift_name("dummyVar")));

I guess Xcode was still seeing an older build of the shared Kotlin module. This is indeed the expected output.

While this seems to be an improvement, issues such as rickclephas/KMP-ObservableViewModel#31 seem to indicate that Bindings should be generated instead of the underlying type variables - in this case String?. I'm using both @NativeCoroutinesState for the properties, as well as @StateViewModel for vm. Is this expected, with manual bindings needing to be used instead?

Yeah bindings are supported for MutableStateFlows:

$viewModel.dummyVar // Binding<String?>

As long as the data type matches the expected type, you shouldn't need to create a binding manually.

LivPNavusoft commented 1 month ago

I appreciate the follow up! I was originally having an issue with the binding where XCode couldn't find the proper initializer, but that may have been a cached issue as I'm seeing the bindings now. Apologies the issue seems to have been unrelated, but I appreciate your time and assistance!