https://kotlinlang.org logo
#multiplatform
Title
# multiplatform
d

Daniele B

09/15/2020, 3:51 PM
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`:
Copy code
@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:
Copy code
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`:
Copy code
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))
  }
}
👍 8
K 3
👏 4
👀 1
🎉 24
k

Kurt Renzo Acosta

09/15/2020, 3:54 PM
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

09/15/2020, 3:55 PM
Yes, I am using
StateFlow
k

Kurt Renzo Acosta

09/15/2020, 3:57 PM
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

09/15/2020, 5:01 PM
@Kurt Renzo Acosta the
StateFlow
code on iOS is very straightforward. it’s just a “callback” providing the new state.
f

flosch

09/15/2020, 5:28 PM
You are never canceling the job that is launched in onChange though.
👍 1
k

Kurt Renzo Acosta

09/15/2020, 5:28 PM
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

09/15/2020, 5:31 PM
@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
f

flosch

09/15/2020, 5:36 PM
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

09/15/2020, 5:39 PM
Just treat
CoreViewModel
as an Objective-C object, as it’s defined in an Objective-C framework. Feel free to share snippets.
l

louiscad

09/15/2020, 5:40 PM
StateFlow is still a Flow and should work wrapped in CFlow linked above.
f

flosch

09/15/2020, 5:50 PM
Yea well since
CFlow
uses a
Closable
, its the same principle, it needs to be cleaned up at some point. 👍
d

Daniele B

09/15/2020, 5:53 PM
maybe we should ask the opinion of the Zar @elizarov
e

elizarov

09/16/2020, 7:57 AM
Sorry, I'm not an iOS/Swift expert, so I cannot really contribute any new ideas here.
d

Daniele B

09/16/2020, 3:37 PM
f

flosch

09/16/2020, 3:41 PM
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

09/16/2020, 4:01 PM
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

09/16/2020, 4:14 PM
@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.
k

Kurt Renzo Acosta

09/17/2020, 9:45 PM
FYI
StateFlow
already existed before in the form of a
ConflatedBroadcastChannel
.
👍 1
d

Daniele B

09/18/2020, 1:19 AM
@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?
k

Kurt Renzo Acosta

09/18/2020, 1:19 AM
It’s just a util or a workaround to get it working on iOS. It’s not part of the library
👍 1
d

Daniele B

09/18/2020, 1:20 AM
ok, got it now
@Kurt Renzo Acosta @flosch I just noticed this important difference between
StateFlow
and `ConflatedBroadcastChannel`:
Copy code
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
f

flosch

09/18/2020, 6:22 PM
Yes, you cannot close
StateFlow
, but if you launch it there will still be a
Job
that probably should be cleaned up
d

Daniele B

10/05/2020, 2:09 PM
@rocketraman glad to hear! were you able to write a better code for handling StateFlow on iOS?
r

rocketraman

10/05/2020, 2:17 PM
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

10/05/2020, 2:44 PM
I am still waiting for this to be answered by the JetBrains team: https://youtrack.jetbrains.com/issue/KT-41953
l

louiscad

10/05/2020, 2:47 PM
@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

10/05/2020, 2:49 PM
@louiscad the CFlow code deals with a closable Flow, but as far as I understand StateFlow is not closable
r

rocketraman

10/05/2020, 2:49 PM
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.
☝️ 2
d

Daniele B

10/05/2020, 3:44 PM
@rocketraman @louiscad I have modified this:
Copy code
fun onChange(provideNewState: ((AppState) -> Unit)) {
        stateFlow.onEach {
            provideNewState(it)
        }.launchIn(
            CoroutineScope(Dispatchers.Main)
        )
    }
into this:
Copy code
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

10/05/2020, 3:47 PM
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

10/05/2020, 3:48 PM
isn’t
Dispatchers.Main
the UI dispatcher also recognised on iOS?
r

rocketraman

10/05/2020, 3:49 PM
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

10/05/2020, 3:56 PM
@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

10/05/2020, 3:57 PM
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

10/05/2020, 3:58 PM
I am actually planning to have just one ViewModel and one StateFlow
r

rocketraman

10/05/2020, 3:59 PM
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

10/05/2020, 4:28 PM
yes, I am just using it on iOS
on Android, I am just using JetpackCompose’s
collectAsState()
r

rocketraman

10/05/2020, 4:29 PM
Exactly my point 😉
l

louiscad

11/13/2020, 10:52 AM
@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.
k

Kurt Renzo Acosta

11/13/2020, 11:03 AM
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

12/10/2020, 10:14 PM
3 Views