Hi, I have a question regarding state management i...
# compose
f
Hi, I have a question regarding state management in compose. I'm still a bit unsure when to use plain state holder classes and when to have logic in the composables. Let me give you an example: I have a
VideosList
composable that receives
isAutoPlayEnabled
as a parameter.
isAutoPlayEnabled
comes from the
viewModel
, it's source of truth is a preference data store. Now I need to react to changes in this parameter to play/pause videos. When I put the logic in the
VideosList
composable, I can have a
LaunchedEffect
that is keyed on
isAutoPlayEnabled
, it will run every time that property changes. Now, because there are more states involved (e.g. lifecycle onResume/onPause), I would like to move this (and other logic) into a state holder class, let's call it
VideoListState
. But how can I listen to changes of
isAutoPlayEnabled
inside this class? I can re-instantiate it whenever the param changes, but that seems a bit wasteful, especially considering that I would also like to subscribe to lifecycle events in the class. Optimally, I could pass a
Flow<Boolean>
and I guess I could get that by either exposing a Flow from the viewModel and passing that down or by using a combination of
rememberUpdatedState
and
snapshotFlow
.However both options somehow feel weird. Maybe I'm missing something obvious here that would make all of this simpler?
a
One thing you could try is making an anonymously implemented state holder class. By that I mean doing something like creating an interface for your state holder:
Copy code
interface VideoListState {
    val something: String

    fun doSomething()

    // other methods and values of the state holder
}
And then having a
@Composable rememberVideoListState(): VideoListState
that looks something like this:
Copy code
@Composable
fun rememberVideoListState(
    isAutoPlayEnabled: Boolean
): VideoListState {
    var internalState by remember { mutableStateOf() }

    var otherInternalState by rememberSaveable { mutableStateOf() }

    LaunchedEffect(isAutoPlayEnabled) {
        // do something
    }

    return remember {
        object : VideoListState {
            override val something: get() = // ...

            override fun doSomething() {

            }
        }
    }
}
So to anyone consuming
VideoListState
, it looks like you have a normal class implementation, but the implementation can use more composition tools without the recreating problem.
f
Ok, yeah thanks. I do kinda wish though that I would not have to use
LaunchedEffect
. I don't really need
isAutoPlayEnabled
anywhere in the UI, it's just for the side-effect of playing/pausing, hence subscribing to a flow would feel more natural to me.
I had a similar issue btw with an
isMuted
state. This state was held higher up than the
VideoList
because it needed to be changed there. What I ended up was creating a
VideoMutedState
class that basically just wraps a
mutableStateOf(muted)
. I then pass this into
VideoListState
and finally into
VideoState
where I can properly subscribe to changes using
snapshotFlow
because it's backed by compose state.
Still not sure that's the proper way though
I don't find much about these kind of situations in the docs. Mostly that is because you either have a truly unique situation at hand or because you're doing something wrong. And it feels more like the latter here 😄
a
If a
Flow
makes more sense,
snapshotFlow
is the tool for that. I’m not sure if there’s a single “proper” way, if you’re taking care to avoid cyclical dependency loops and have a single source of truth. And it can be tricky to find the place where you have all of the info you need to make some decision
f
But
snapshotFlow
requires compose state which
isAutoPlayEnabled
is not in my example. I could use
rememberUpdatedState
to create a compose state but that feels a bit off?
a
Yes exactly! If you have a plain argument to a
@Composable
function, you can use
rememberUpdatedState
to get it back to a
State
that you can observe in a
snapshotFlow