https://kotlinlang.org logo
t

timkranen

06/30/2022, 3:38 PM
Hey! Bit of an in depth question, but I’m super interested in hearing what your approach is on this problem: I’m currently sharing all of my logic from the ViewModel upwards in KMM. So I’m using an actual/expect construct for the VM, where the android VM uses the jetpack viewmodel, and iOS uses a ‘default’ class. Then I’m using Koin for dependency injection, which works great on Android since there is a simple
viewModel
scope. But I’m running into problems on iOS with regards to the lifecycle.. Currently my VM is marked as a singleton for iOS, however there are a couple of problems that arise: • Initially I thought it would be a good idea to cancel the scope in the viewModel on viewDisappeared, but when I’m navigating back to that same screen Koin will provide the same VM with a cancelled coroutine scope, so that breaks stuff.. • Changing the viewModel to be a factory solves this problem, but then I can’t retain state across screen navigation, which isn’t the end of the world and I can make it work, but it seems like it’s limiting? It also introduces the problem of somehow having to manually save state like scroll positions What are your approaches to the lifecycle of viewmodels on iOS? When do you cancel the scope? Does the VM retain state across navigation?
c

clark

07/01/2022, 4:37 PM
Our approach has been to create view models independently on iOS and Android as they do have different lifecycles and different ways of interacting in some cases. We expose flows from a model cache and our repositories update the model cache with the data they fetch. Those model cache flows are consumed by the platform independent view models.
m

Matti MK

07/04/2022, 5:32 AM
I came across this the other day, might be relevant for you: https://www.marcogomiero.com/posts/2022/improved-kmm-shared-app-arch/ You can see an example of cancellation you mentioned in the
KaMPKit
sample: https://github.com/touchlab/KaMPKit/blob/main/shared/src/iosMain/kotlin/co/touchlab/kampkit/models/ViewModel.kt
Please do keep us updated if you continue to research the issue and come across a solution that you find suitable 👍
t

timkranen

07/04/2022, 8:08 AM
So, looking at the first article you linked @Matti MK, I think that approach could work but it’s not exactly the same as sharing the viewmodel across platforms. What I did for now, and what works for our usecase, is shortening the lifecycle of the iOS viewmodel. What I figured out was that functions like
.onDisappeared
aren’t called when navigating to detail screens on iOS. So what we can do is: • Initialize a new viewmodel on every
.onAppear
, can keep this viewmodel in memory through an
ObservableObject
. Whenever we navigate to a detail view, the VM is retained, so coming back to the original screen leaves the original state. But when navigating away from the screen entirely
.onDisappeared
is called, the VM is cleaned up, and once
.onAppear
gets called again a new instance of the VM is created. • We use Koru to map kotlin Flow to swift combine, this basically allows us to keep all the view state logic inside the VM except for: observing state changes (through combine).
I am curious however, I’m basing a lot of my project on the KaMPKit project, and they still use a
single
for the iOS VM. I wonder how they deal with VM instances that have a cancelled scope in the ViewModel..
m

Matti MK

07/04/2022, 9:00 AM
I’m doing the same as you, but I’m mainly keeping an eye on memory leaks and memory consumption: works well. I’ve not yet had serious state issues. However, I’m leaning more and more towards sharing UCs instead of VMs and extracting as much business logic as possible to the UCs themselves
Currently I’m doing something like this on iOS:
SwiftUI
->
ViewModelWrapper
->
KMMSharedViewModel
->
UC
->
data layer
Here the VM wrapper does what you mentioned:
observing state changes (through combine)
t

timkranen

07/04/2022, 9:04 AM
That is indeed very similar to my setup. Why are you considering moving move logic the UCs? I feel like sharing the view state management in the VM provides a lot of benefit
m

Matti MK

07/04/2022, 9:08 AM
Depending on the scenario really. But it’s mainly for the reasons you mentioned. I feel having “native” state management in VMs would be a better option, however, I’ve not yet looked into it more closely. Moreover, I’m moving towards having all business logic into UCs while VMs would simply have logic such as
showError
and
showLoading
etc on VMs. KMM VM sharing does work very nicely towards Android, though.
That being said, I’ve had an app in production since December, with the architecture we’ve discussed, and I’ve not heard of any performance issues. Extremely simple piece of software though
Oh, one more thing to mention: testing should be easier in iOS too I imagine. To my lament I cannot remember anymore what was the issue, but doing “unit tests” for UI on iOS was a serious pain. This is mainly because
@Published
properties cannot be mocked/doubled for tests. This means that you’ll end up with a form of an integration test. I don’t remember why it didn’t seem feasible to mock out the KMM VM in the iOS VM wrapper, but there was an issue. When I mention mocking here, it’s not the same as mocking on Android as iOS doesn’t have reflection. “Mocks” need to be generated beforehand and IIRC the mocking tools (
Cuckoo
for example) wasn’t able to access the KMM cocoapods dep. But my memory on this is a bit vague so it might be that this is not the case really. And might be that it’s the same issue if iOS just consumes UC directly: at least the
@Published
wrapper issue doesn’t change.
Did you find the solution to your state issue? I’ve been pondering this and wondered how you are dealing with lifecycles on the iOS side? As in, if you have VMs as `StateObject`s and navigate between screens, then do those keep state? That’s what they’d suppose to be doing, if you are following SwiftUI navigation or using
UIViewController
based navigation with SwiftUI views. In case you haven’t tested the above, then I’d start there. If all native iOS components don’t keep state, then you might be doing something incorrect in your iOS navigation.
5 Views