rickclephas / KMP-ObservableViewModel

Library to use AndroidX/Kotlin ViewModels with SwiftUI
MIT License
550 stars 28 forks source link

The flow is not converted to property even after using @NativeCoroutinesState #14

Closed bibutikoley closed 1 year ago

bibutikoley commented 1 year ago

The library is not generating the correct @property for the defined StateFlow.

image image

Is there a script or command that needs to be executed for the correct code to be generated?

joreilly commented 1 year ago

Example of it's use here in case it helps https://github.com/joreilly/Confetti/blob/main/iosApp/iosApp/ContentView.swift

Are you using @StateViewModel?

On Mon 23 Jan 2023, 13:11 Bibuti Koley, @.***> wrote:

The library is not generating the correct @Property https://github.com/Property for the defined StateFlow.

[image: image] https://user-images.githubusercontent.com/20051993/214047814-d50c0b60-238e-4ea3-a0f8-a4e4524a6dee.png

[image: image] https://user-images.githubusercontent.com/20051993/214047942-1e21b725-3ff8-4b66-b856-367fc7efcade.png

Is there a script or command that needs to be executed for the correct code to be generated?

— Reply to this email directly, view it on GitHub https://github.com/rickclephas/KMM-ViewModel/issues/14, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAABRHRTAPYBMOKKY4GFRZTWTZ7R3ANCNFSM6AAAAAAUD2L73I . You are receiving this because you are subscribed to this thread.Message ID: @.***>

bibutikoley commented 1 year ago

@joreilly Yes.. I referred the Confetti Repository. Below is my code snippet.

image image
rickclephas commented 1 year ago

Hi @bibutikoley! The properties generated by KMP-NativeCoroutines are extensions properties. Hence they will have their own categorie in the Objective-C header. Are you able to access the randomNumbers property from Swift? If not, could you share your build.gradle(.kts) file?

bibutikoley commented 1 year ago

Hello @rickclephas, I'm not able to access the randomNumbers property. It gives me an error. "Cannot assign to property: 'randomNumbers' is a get-only property"

image

Here is my build.gradle.kts file (shared module)

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    id("com.android.library")

    kotlin("plugin.serialization") version "1.8.0"

    //Native Coroutines Dependency
    id("com.google.devtools.ksp") version "1.8.0-1.0.8"
    id("com.rickclephas.kmp.nativecoroutines") version "1.0.0-ALPHA-3"

}

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

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

    sourceSets {

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

        val commonMain by getting {
            dependencies {

                implementation("com.rickclephas.kmm:kmm-viewmodel-core:1.0.0-ALPHA-3")

                implementation("io.ktor:ktor-client-core:2.2.1")
                implementation("io.ktor:ktor-client-cio:2.2.1")
                implementation("io.ktor:ktor-client-serialization:2.2.1")
                implementation("io.ktor:ktor-client-content-negotiation:2.2.1")
                implementation("io.ktor:ktor-serialization-kotlinx-json:2.2.1")

            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-android:2.2.1")
                implementation("androidx.core:core-ktx:1.9.0")
                implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
            }
        }
        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-ios:2.2.1")
            }
        }
        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 = "com.example.kmparch_mvi"
    compileSdk = 33
    defaultConfig {
        minSdk = 25
        targetSdk = 33
    }
}
bibutikoley commented 1 year ago

@joreilly @rickclephas Thank You for the support and Sorry for the confusion. I placed the KMMViewModel.swift file in a wrong directory. The Code seems to be working fine.

But would like to know if inheritance is supported or not?

rickclephas commented 1 year ago

Interesting error message. Anyway I think it's because viewModel is the Projection. Could you try the following?:

print("ViewModel -> \(self.viewModel.randomNumbers)")
rickclephas commented 1 year ago

What exactly do you mean by "inheritance"? You can subclass your Kotlin ViewModel in Swift (if that's what you mean) like in the sample: https://github.com/rickclephas/KMM-ViewModel/blob/3c73e14db2576edf8b9e8539e80a4fb3f4ff2090/sample/iosApp/KMMViewModelSample/TimeTravelViewModel.swift#L10-L19

bibutikoley commented 1 year ago

@rickclephas By Inheritance I mean,

BaseViewModel : KMMViewModel()

and RandomViewModel : BaseViewModel()

Is this possible?

rickclephas commented 1 year ago

Yeah that should be possible as well.

bibutikoley commented 1 year ago

@rickclephas Thanks for the help.. closing this issue.

bibutikoley commented 1 year ago

@rickclephas Can we use SharedFlow with this Library?

image

Something like this for one-time events

rickclephas commented 1 year ago

No, SharedFlows aren't currently supported.

Could you share some more information about this use case? How would you expect to consume/use such a Flow in iOS (and Android)?

bibutikoley commented 1 year ago

@rickclephas I want to perform navigation in the app. (i.e. - One time event). I would request you to kindly clone this repo - https://github.com/bibutikoley/KmpArch

This repo is simply fetching the data from newsapi and displaying it in the list view and tap of the item user navigates to details page.

I'm using NativeCoroutines and KMM-ViewModel for the same. My objective is to make the android and iOS use the same codebase in shared module.

For android it works as expected but unable to achieve it in iOS.

Also, the library does not generate the extension properties if ViewModel is in a different Module. For. eg. In the repo - If I extend NewsListingViewModel from BaseViewModel it works(same module), But If I extend it from KmpViewModel it does not work(different module).

rickclephas commented 1 year ago

Thanks for the sample code! How would you normally implement such an architecture in pure Swift? I am struggling to see/understand the Swift version of such an architecture.

Also, the library does not generate the extension properties if ViewModel is in a different Module.

KMP-NativeCoroutines only generates the required Kotlin code. When using multiple modules you'll need to make sure these modules are exported to your iOS project. See the export option in the CocoaPods plugin documentation.

bibutikoley commented 1 year ago

@rickclephas Thanks for looking into the code. The architecture was not meant for pure swift but to achieve a MVI based Architecture with Shared Code(KMM).

The above architecture is referred from this link - Demo - https://github.com/fededri/kmm-demo Explanation of the architecture - https://proandroiddev.com/kotlin-multiplatform-mobile-and-how-share-viewmodel-an-architecture-proposal-b6f86b61abf9.

I'm trying to mix the KMM-ViewModel, NativeCoroutines and The Architecture above to move business logic and ui-states of the app to shared module and the UI(android and iOS) will receive the ui-states directly from the viewmodel and perform respective events.

I would request you to please take a look at the above links and share your feedback.

Also, when working with generics in KMM-ViewModel do we need to cast the value to access it in swift code or is there any other solution available? ref.https://github.com/rickclephas/KMM-ViewModel/issues/15

bibutikoley commented 1 year ago

@rickclephas Is there a way to directly initialize the @ObservedViewModel var newsListingViewModel: NewsListingViewModel in the 'NewsListView' from the above example?

Currently we are passing the value from with init(viewModel: ObservableViewModel.Projection)

Please let me know your thoughts on this

rickclephas commented 1 year ago

You can use the same approach and pass the NewsListingViewModel directly as well. However keep in mind that you can only do this once for a specific view model. Once the VM is wrapped you'll have to use the projection instead.

init(viewModel: NewsListingViewModel) {
    self._newsListingViewModel = ObservedViewModel(wrappedValue: viewModel)
}
bibutikoley commented 1 year ago

Hi @rickclephas, I tried using the code snippet that you have shared. But unfortunately with this approach the Flow in the viewModel(annotated with NativeCoroutinesState) is not receiving any items emitted by the ViewModel.

import shared
import KMMViewModelSwiftUI
import KMMViewModelCore

struct NewsListView: View {

    @ObservedViewModel var newsListingViewModel: NewsListingViewModel

    @State private var isActive = false

    init(vm: NewsListingViewModel = NewsListingViewModel()) {
        self._newsListingViewModel = ObservedViewModel(wrappedValue: vm)
        processEvent(event: (vm.observeEvents as? NewsListingEvents))
    }

    @State private var isShowingDetailView = false

    var body: some View {
        NavigationView {
            VStack {
                ScrollView {
                    Text("Latest News")
                        .padding(20)
                    ForEach((newsListingViewModel.observeRenderState as? NewsListingRenderState)?.newsArticleList ?? [], id: \.self) { item in
                        VStack {
                            NewsListCell(item: item, onTap: { article in
                                newsListingViewModel.action(action: NewsListingActions.UserActionsSelectNewsArticle(newsArticle:article))
                            }).background(
                                NavigationLink(
                                    destination: NewsDetailsView(newsArticle: item),
                                    isActive: $isShowingDetailView
                                ) {
                                    EmptyView()
                                }
                            )
                        }.padding(.bottom, 16)
                            .padding([.leading, .trailing], 16)
                            .background(Color.white)
                            .clipped()
                            .shadow(color: Color.gray.opacity(0.3), radius: 2, x: 0, y: 2)
                    }
                }
            }
        }
    }

    private func processEvent(event: NewsListingEvents?) {
        if event == nil { return }
        switch event {
        case let event as NewsListingEvents.OpenSelectedNews : do {
            print("Event Received -> \(event)")
            break
        }
        default:
            print("Event not recognized")
            break
        }
    }
}

In the above eg. vm.observeEvents is the flow which gets updated by the ViewModel but the item is not received in the UI. Please check the above code snippet and please share your thoughts. Thanks!

rickclephas commented 1 year ago

You are only processing a single event during the initialisation of the view. Depending on the implementation of the Flow this might be null or an initial event.

Only the body of your view will be reevaluated by SwiftUI upon state changes. I think you are looking for onreceive(_:perform:).