d

    Daniele B

    2 years ago
    Today it was a great milestone! I was able to run exactly the same shared ViewModel on both Android and iOS, using
    StateFlow
    !!! I can easily say 85% of the code is on Kotlin Multiplatform, and 15% only on the platform-specific declarative UI (Compose and SwiftUI)! on Android, it uses Compose's collectAsState:
    @Composable
    fun MainLayout(coreModel: CoreViewModel) {
        val appState by coreModel.stateFlow.collectAsState()
        ...
    }
    on iOS, it uses a very simple class extending
    ObservableObject
    , where a StateFlow “listener” is initialized:
    class AppViewModel: ObservableObject {
      let coreModel : CoreViewModel = CoreViewModel()
      @Published var appState : AppState = AppState()
         
      init() {
          coreModel.onChange { newState in
              self.appState = newState
          }
       }
    }
    this is the shared ViewModel:
    class CoreViewModel {
    
      internal val mutableStateFlow: MutableStateFlow(AppState())
      val stateFlow: StateFlow<AppState>
          get() = mutableStateFlow
    
      fun onChange(provideNewState: ((AppState) -> Unit)) {
          stateFlow.onEach {
              provideNewState.invoke(it)
          }.launchIn(CoroutineScope(Dispatchers.Main))
      }
    }
    Kurt Renzo Acosta

    Kurt Renzo Acosta

    2 years ago
    Congrats! Are you using
    Flow
    on your ViewModels? If so, were you able to consume it on iOS? I'm still doing the
    CFlow
    hack and I wanted to get rid of it soon
    d

    Daniele B

    2 years ago
    Yes, I am using
    StateFlow
    Kurt Renzo Acosta

    Kurt Renzo Acosta

    2 years ago
    How are you observing it on iOS side? Edit: Just saw your edit with the code
    Yeah still the same pattern where we have to do a few tweaks to make it consumable to iOS. Suspend functions already get translated into a lambda. I hope we get the same for Flow operators too
    d

    Daniele B

    2 years ago
    @Kurt Renzo Acosta the
    StateFlow
    code on iOS is very straightforward. it’s just a “callback” providing the new state.
    flosch

    flosch

    2 years ago
    You are never canceling the job that is launched in onChange though.
    Kurt Renzo Acosta

    Kurt Renzo Acosta

    2 years ago
    Yup. I was looking for a solution where we don’t have to create a callback and just use the
    StateFlow
    like we use it on Android. I have the same pattern here with the help of KotlinConf’s CFlow so that I don’t have to create a callback for every
    StateFlow
    . I just hoped it would be possible to avoid it since suspend functions can be used in iOS but I guess not for
    Flow
    yet.
    d

    Daniele B

    2 years ago
    @Kurt Renzo Acosta I am not aware of a different way to collect StateFlow on iOS. But I guess it will arrive sometime soon, and it’s just a matter to update a few lines of code.
    @flosch if you have any suggestion, it’s very welcome
    flosch

    flosch

    2 years ago
    I don’t know the inter-capabilities of iOS/kmpp, but since swift has a deinit you could create a DeallocWatcher that cancels the
    CoreViewModel
    scope
    d

    Daniele B

    2 years ago
    Just treat
    CoreViewModel
    as an Objective-C object, as it’s defined in an Objective-C framework. Feel free to share snippets.
    louiscad

    louiscad

    2 years ago
    StateFlow is still a Flow and should work wrapped in CFlow linked above.
    flosch

    flosch

    2 years ago
    Yea well since
    CFlow
    uses a
    Closable
    , its the same principle, it needs to be cleaned up at some point. 👍
    d

    Daniele B

    2 years ago
    maybe we should ask the opinion of the Zar @elizarov
    elizarov

    elizarov

    2 years ago
    Sorry, I'm not an iOS/Swift expert, so I cannot really contribute any new ideas here.
    d

    Daniele B

    2 years ago
    flosch

    flosch

    2 years ago
    Why create an issue for that though 🤔 In the kotlinconf app you can find the cleanup being done. And even if you wanted to somehow automatically close the
    CFlow
    (cancel the
    Job
    ), this would be an implementation detail.
    a

    audriusk

    2 years ago
    In summary, as you know, on JetpackCompose there is a very neat way to collect a StateFlow:
    That's because Android team (from Google) has incorporated Coroutines support into JetpackCompose. I do not expect same support from Apple 😄 I think community will have to step up and create some sort of components for SwiftUI to bind
    StateFlow
    . Maybe it's possible to wrap it with Swift's Combine framework 🤔 (There might be some problems since SwiftCombine is Swift-only and Kotlin Native has only supports Objective-C)
    d

    Daniele B

    2 years ago
    @flosch I think it would be great to have a one-line
    collectAsState()
    , so that developers don’t have to manage the cleanup themselves. But I am not sure if it’s feasible. Let’s see what’s the reply. I also think,
    StateFlow
    didn’t exist at the time the kotlinconf app was made.
    @audriusk maybe using Combine under the hood? Not sure. From a Kotlin developer point of view, there shouldn’t be the need to deal with Combine directly. It would make sense to only having to care about the Kotlin Flow.
    Kurt Renzo Acosta

    Kurt Renzo Acosta

    2 years ago
    FYI
    StateFlow
    already existed before in the form of a
    ConflatedBroadcastChannel
    .
    d

    Daniele B

    2 years ago
    @Kurt Renzo Acosta by the way, I was looking for
    CFlow
    in the Kotlin documentation, but I cannot find it. Was it also renamed to something else?
    Kurt Renzo Acosta

    Kurt Renzo Acosta

    2 years ago
    It’s just a util or a workaround to get it working on iOS. It’s not part of the library
    d

    Daniele B

    2 years ago
    ok, got it now
    @Kurt Renzo Acosta @flosch I just noticed this important difference between
    StateFlow
    and ConflatedBroadcastChannel:
    StateFlow cannot be currently closed like ConflatedBroadcastChannel and can never represent a failure
    https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/
    StateFlow seems “smarter” than ConflatedBroadcastChannel
    flosch

    flosch

    2 years ago
    Yes, you cannot close
    StateFlow
    , but if you launch it there will still be a
    Job
    that probably should be cleaned up
    r

    rocketraman

    1 year ago
    @Daniele B Shout-out for your post!! It, and the other comments in this thread, got me started in the right direction. Combining your pattern with
    CFlow
    mentioned above from the Kotlin MPP conference app for properly closing the job associated with reading the
    StateFlow
    , and Kodein for injecting the shared view model into both Android and Swift code, and I've successfully gotten an MPP app with iOS and Android views driven by
    StateFlow
    working!
    d

    Daniele B

    1 year ago
    @rocketraman glad to hear! were you able to write a better code for handling StateFlow on iOS?
    r

    rocketraman

    1 year ago
    Not sure if you could call it better... it was just pulling together some pieces. I copied the code of
    CFlow
    directly from the Kotlin Conference app along with the native dispatcher from the same place:1. https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/mobileMain/kotlin/org/jetbrains/kotlinconf/FlowUtils.kt 2. https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/iosMain/kotlin/org/jetbrains/kotlinconf/DispatcherNative.kt I injected the view model's
    CFlow
    adapter into the iOS
    ObserverObject
    (on Android the view model is injected directly). From there, using
    StateFlow
    on Android normally, and on iOS using CFlow's watch to set the
    @Published
    vars just worked.
    All the extra rigamarole should go away when https://github.com/Kotlin/kotlinx.coroutines/issues/470 is implemented.
    d

    Daniele B

    1 year ago
    I am still waiting for this to be answered by the JetBrains team:https://youtrack.jetbrains.com/issue/KT-41953
    louiscad

    louiscad

    1 year ago
    @Daniele B Instead of waiting, you could make your own solution (which can be to just copy/paste
    CFlow
    ), because they might not be able to answer the way you hope for in the timeframe you're looking for.
    d

    Daniele B

    1 year ago
    @louiscad the CFlow code deals with a closable Flow, but as far as I understand StateFlow is not closable
    r

    rocketraman

    1 year ago
    CFlow
    seems to be the way to go as of now. On that, you just call
    close
    in the appropriate callbacks in Swift code. The conference app has some examples showing this.
    @Daniele B You are closing the Job that reads the StateFlow, not the StateFlow itself.
    d

    Daniele B

    1 year ago
    @rocketraman @louiscad I have modified this:
    fun onChange(provideNewState: ((AppState) -> Unit)) {
            stateFlow.onEach {
                provideNewState(it)
            }.launchIn(
                CoroutineScope(Dispatchers.Main)
            )
        }
    into this:
    fun onChange(provideNewState: ((AppState) -> Unit)) : Closeable {
            val job = Job()
            stateFlow.onEach {
                provideNewState(it)
            }.launchIn(
                CoroutineScope(Dispatchers.Main + job)
            )
            return object : Closeable {
                override fun close() {
                    job.cancel()
                }
            }
        }
    do you think it’s ok now?
    r

    rocketraman

    1 year ago
    Dispatchers.Main
    should probably be an expect/actual which points to this native dispatcher in iOS: https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/iosMain/kotlin/org/jetbrains/kotlinconf/DispatcherNative.kt
    d

    Daniele B

    1 year ago
    isn’t
    Dispatchers.Main
    the UI dispatcher also recognised on iOS?
    r

    rocketraman

    1 year ago
    Otherwise, yes, that's essentially the same code as
    CFlow
    , except
    CFlow
    is nice as it wraps any
    Flow
    , so you don't have to add that
    onChange
    method directly to your view state.
    For the dispatcher, I think there are potential issues with the built-in
    Main
    dispatcher. See: https://github.com/Kotlin/kotlinx.coroutines/issues/470.
    d

    Daniele B

    1 year ago
    @rocketraman I was thinking the implementation is comparable: in both cases a method is defined as closeable. In the kotlinconf app it’s called “watch”, in my app it’s called “onChange”.
    r

    rocketraman

    1 year ago
    Yeah its the same, except yours is on the View right? If you have more than one view its not DRY. Abstracting it into a wrapper class puts it in one place regardless of how many views you have.
    correction: view model
    d

    Daniele B

    1 year ago
    I am actually planning to have just one ViewModel and one StateFlow
    r

    rocketraman

    1 year ago
    In that case, its the same, yeah, though I still think separating the concerns is a good idea i.e. the throwaway code that should eventually go away is independent from the code that should stick around.
    Plus: on android you shouldn't be using that method as you should be using different scopes there (e.g. lifecycleScope), and so abstracting that method away into a separate class like
    CFlow
    makes it easier to not mistakenly use that
    onChange
    method where it shouldn't be used.
    d

    Daniele B

    1 year ago
    yes, I am just using it on iOS
    on Android, I am just using JetpackCompose’s
    collectAsState()
    r

    rocketraman

    1 year ago
    Exactly my point 😉
    louiscad

    louiscad

    1 year ago
    @dagomni There's already great suggestions for folks above. TL;DR: You can copy and use
    CFlow
    from KotlinConf app (link above) or edit it to suit your needs. Is it not enough?
    You can see the
    Flow
    type from Swift, so you can pass it to
    CFlow
    from Swift before getting the values in iOS code.
    Kurt Renzo Acosta

    Kurt Renzo Acosta

    1 year ago
    I’m using
    CFlow
    on Android and iOS and I’m fully sharing my VMs. No need for a wrapper on iOS.
    d

    Daniele B

    1 year ago