Hi! If I have a `LazyList` in Android Compose, and...
# orbit-mvi
d
Hi! If I have a
LazyList
in Android Compose, and I want to manage the state of each of those items ONLY when it's displayed (I guess with a separate container for each -- there's also a difference in the state logic for different types of items...), how could I achieve this with Orbit?
m
I think this is more of a
Compose
question. Speaking from experience - you can have a separate view model per each item in the list, keyed by the item ID or something similar using Compose's
viewModel
function. Compose will keep track of the ViewModels for you.
d
I really want to release the resources taken up by the container and it's subscriptions (each one needs to listen to two external flows that change it's state too... apart from user actions and there could potentially be very long lists being scrolled through...)
I was thinking of some kind of
close()
function on the container... or maybe by losing reference to it, it automatically cancels all it's coroutines?
m
use
repeatOnSubscription
d
And besides, I can't really do like you suggested @Mikolaj Leszczynski, since my details view uses a different details entity than my list views... and I need to manage the state for both in one place... I was thinking of a WeakHashMap in a class scoped to the activity to contain the containers and retreive them for each type of view/item
m
when the item goes away, subscriptions will be dropped and
repeatOnSubscription
ensures that the flow collection will be cancelled
d
And then release them... When the
item goes away
means loosing it's reference and being GCed, will that still cancel the flows?
m
There are two elements to this: 1. Coroutines collecting the state and side effects are cancelled when the compose view leaves the hierarchy 2. Having no subscribers triggers
repeatOnSubscription
to cancel its block, meaning anything you collect there will be cancelled
Other than that, the ViewModels will live as long as their
ViewModelStoreOwner
is alive
Or more specifically - an Orbit container lives as long as the
coroutineScope
it's created with. If you use Android's VM's that is the ViewModel Scope. If you want custom lifecycle handling, you have two options: 1. Create the containers using a scope different to the ViewModel and manage that yourself 2. Create a custom ViewModelStoreOwner to manage the ViewModel lifecycle
d
an Orbit container lives as long as the
coroutineScope
it's created with.
sounds like a contradiction to
1. Coroutines collecting the state and side effects are cancelled when the compose view leaves the hierarchy
2. Having no subscribers triggers
repeatOnSubscription
to cancel its block, meaning anything you collect there will be cancelled
?
m
This is entirely accurate 🙂 The ViewModel and therefore the container still lives after all subscriptions go away, by design - to deal with rotation etc. • Only intents with
repeatOnSubscription
are cancelled when subscriptions go away • Everything else continues running until the ViewModel's
coroutineScope
is cancelled.
d
• Everything else continues running until the ViewModel's
coroutineScope
is cancelled.
Wow, then it could be nice to have
ContainerHost
have a default CoroutineScope generated along with a
cancel()
or
close()
if managed by it... but I guess it would get dangerous if one would pass the viewModel's scope and accidentally cancel it...!
I could maybe make my own ScopedContainerHost...
m
just don't use ViewModels.
you can create a container using a custom scope using
someScope.container(...)
I don't think you need anything more than that
d
yeah... but then I'd still need extra logic to recreate a new container if it's scope is cancelled, I can't just re-use the one stored in the map... I would have expected it to stop everything when unsubscribed on the UI, and resart itself when resubscribed.
m
But - out. of curiosity why is
Everything else continues running until the ViewModel's
coroutineScope
is cancelled.
a problem?
if you collect flows, collect them in
repeatOnSubscription
any one-off intents will eventually stop running right?
then the orbit event loop is suspended until the next intent
d
Good question... I don't really know what
Everything else
means here to be able to say if too many of them running would cause a lot of overhead...
m
it shouldn't be resource hungry - other than keeping the
state
in memory
We use it at work in that manner and it's not causing issues 🙂
with ViewModels
d
So basically, just keeping a map of container hosts, and subscribing/unsubscribing to them from the UI is the way to go... the only last concern is if they are GCed from the WeakHashMap, will they continue using resources unless they are explicitly cancelled.
The viewModel WILL still be alive while scrolling
m
My recommendation is - just go with one ViewModel per item and let Compose manage it. Make sure you collect any flows in
repeatOnSubscription
to make sure they're cancelled. If you need to optimise, you can always do it later.
unless your item states are massive, my gut feeling is the overhead of doing it like this will be minimal.
d
just go with one ViewModel per item and let Compose manage it
How do I share them between the various list/details views? ONE needs to be for a unique item that can have either a summary or a details data class as it's base.
m
Good practice dictates to use a persistence layer for this sort of thing.
Then you don't need to share a ViewModel
but if you REALLY need to, and are using a single activity for both, it should be possible as the view model store owner is the same (the activity)
d
So viewmodels created by compose are unique by the id of the item they contain, and reused for the same id? Or are you talking about something with Orbit?
The activity is the same, but different fragments (I'm porting the code to Compose...)
m
viewmodels in Compose are reused within: 1. The
NavBackStackEntry
if you're using Compose Navigation 2. Within the
ViewModelStoreOwner
which would be the activity in a single activity app - or individual fragments
d
It seems from that article that they DO stay in memory as long as the ViewModelStoreOwner exists... meaning that if I create 500 of them after scrolling through the list, they'll all still be living... unless you NAVIGATE to them (which doesn't apply to lists) and use it as the ViewModelStoreOwner....
Or manage their lifecycle yourself, then what's the point?
m
they'll all still be living.
Define living 🙂
d
🤔
m
If you do it right: 1. The state will be kept| 2. All orbit coroutines will be suspended and using minimal resources
d
So what I'm gaining is that they won't suddenly be GCed without properly being cancelled?
But still have 500 of them in memory with minimal resource use, and if the user keeps on browsing more lists in the same activity... it'll keep on growing, even though they're not used anymore. It could be that you're right that optimisation at this point is pointless, and that it's negligable overhead. I guess I'll have to test this, thanks for your suggestions!
m
Yeah if all you wanted was to make them go away with the list item, that should be easy using a custom
ViewModelStoreOwner
local to the list - but if you want to share them between the list & detail it becomes a complicated problem
then it's a question of who keeps the view model and when is it dropped
In all honesty, I would share data using a persistence layer instead - this gets around the entire problem of scoping the view model
d
Yeah, but it's a bit complicated, because it's a mixture of server data that needs to be fresh coming it from the view model + from WorkManager tasks running in the background + state on the device... a persistence layer would be working with cached data and also would higher the chances of getting into an invalid state.
But in general, you do have a good point 😃
m
is the server and task data exposed as `StateFlow`s?
d
Not the server data, just the task data
Atomicity is preserved by Orbit in ONE instance for ONE object, but if they're spread out into a few instances + a persistence layer, I'd need to go back to managing it myself.
m
hmmm, can't you just write all the data to the persistence layer and observe on that instead of the individual data sources? Then persistence becomes the single source of truth.
From your description, I'm not sure if it should be the individual ViewModels calling the network at all.
d
Meaning rewriting another Orbit, just using persistence 🤒
m
More like an external service (container?)
Meaning rewriting another Orbit, just using persistence
Sometimes it is what it is. The number of times I had to write and re-write the Orbit library to get it right... 😅
d
Btw, you use viewModel { } with a custom factory in the items { } block? It sounds like the docs don't recommend using viewModel in any other place than the top-level screen composable function... unless you pass a lambda that defers creation to the screen function?