https://kotlinlang.org logo
#flow
Title
# flow
l

Landry Norris

05/20/2022, 6:50 PM
I have an interesting use case I’m looking to support. I’m currently using a MutableStateFlow of a single data class to represent the UI state. I have several video controllers that control several videos. Each one exposes the current timestamp of the video as a Flow<Double>. The user can select one video, and the seekbar should set its position based on the timestamp of the selected video. My current solution is to collect the state flow, get the current controller, and collect the position inside of that. I can then update the state based on the collected value. Is there a better way to handle this?
general structure: state is a MutableStateFlow that holds, among other properties, a list of video controllers (state.value.controllers), and a state.value.currentController, and I need to collect state.value.currentController.positionFlow, which is a Flow<Double>, even if I change state.value.currentController
m

Michael Marshall

05/20/2022, 7:59 PM
I think some extra code snippets might help here
l

Landry Norris

05/20/2022, 8:03 PM
Sure. I have a class that looks like
data class UIState(val controllers: List<Controller>, currentController: Controller? = controllers.firstOrNull())
and a class like
Copy code
class Controller {
    val positionFlow: Flow<Double> = MutableStateflow(0.0)
}
Whenever the position changes, I call positionFlow.update { value } In my code, I have
val state = MutableStateFlow(UIState(...))
. I want to collect the currentController’s positionFlow, but I want it to update whenever I change currentController
Right now, I have
Copy code
state.collect { currentState ->
                currentState.currentController.positionFlow?.collect { position ->
                    //Code that uses position
                }
            }
I’m sure there’s some more clear way to do this without nesting collect calls. If I add other flows to the controller, I’d like the solution to scale well.
I’m also considering moving the controllers list outside of the state, since it isn’t really ‘UI state’, but I’m thinking I should keep the currentController part of the state class.
👍 1
m

Michael Marshall

05/23/2022, 4:54 PM
If the Controllers are actually just data classes then it’s probably okay to leave them in the UI state. If they interact with lifecycle and UI events then I’d move them out
currentController could probably just be an index of the list, so you’re not emitting the same controller from two places in the UIState?
Are you looking for something like this?
Copy code
fun reactToControllerAndPositionChanges() {
        // I assume you have some scope
        val scope = CoroutineScope(Dispatchers.Default)

        // Old way
        scope.launch {
            state.collect { currentState ->
                currentState.currentController?.positionFlow?.collect { position ->
                    positionCode(position)
                }
            }
        }

        // New way
        state
            .flatMapLatest { state ->
                state.currentController?.positionFlow ?: emptyFlow()
            }
            .onEach(::positionCode)
            .launchIn(scope)
    }

    private fun positionCode(position: Double) {
        TODO()
    }
If the UI only shows one video at a time and doesn’t need to know about the whole list of Controllers (which at this point you could call
ControllerState
) you could instead just use
val state = MutableStateFlow(ControllerState(...)
and switch out the controllers info in the business logic behind the scenes instead.
^In this case you could just have
data class ControllerState(val position: Double)
and update the whole
state
whenever the position changes
l

Landry Norris

05/23/2022, 5:28 PM
It will show multiple videos side by side, but the position should only respond to changes in the selected video's controller. I'll definitely look at using flatmap.
👍 1
I guess the best I could describe what I’m looking for is being able to state.combine(state.selected.positionFlow), but of course the combine method wouldn’t be able to update what it’s combining with automatically as state.selected changes.
m

Michael Marshall

05/23/2022, 5:33 PM
Yeah combine is for transforming the results from multiple flows, flatmap definitely makes more sense here. In your original code, I believe you might have been creating memory leaks by collecting a new flow each time the current controller changed, but never cancelling the old ones. Flatmap handles that for you.
l

Landry Norris

05/23/2022, 5:34 PM
That makes sense. I need to study up on the more complex flow operators.
❤️ 1
Using flatMap seems to work perfectly, but I’m noticing my Compose UI takes about a second to react when I select a new video.
m

Michael Marshall

05/24/2022, 6:56 PM
Could it be that the point flows don't emit for a second, so the change doesn't happen until then? You could try using a StateFlow for the point flows in that case
l

Landry Norris

05/24/2022, 6:58 PM
The positionFlow shouldn’t be updating its value when a new video is selected. The positionFlow does use a MutableStateFlow under the hood.
👍 1
From my understanding, when I update state, it will trigger onEach. Is this correct? That should be what causes the positionCode to run.
Now that I think about it, I know Compose Debug mode is supposed to run a bit slower. I’m having CI build a production version now. I’m wondering if that’s the cause.
👍 1
m

Michael Marshall

05/24/2022, 7:04 PM
When you update the screen, the chosen position flow will change. When that position flow emits, onEach will run
It could be debug causing it
l

Landry Norris

05/24/2022, 7:06 PM
Just added logs when I call state.update and when onEach runs. The difference is 0.002 seconds, so I think it’s safe to say the delay in rendering is Compose, not the flatMap. Thank you for helping me with this.
m

Michael Marshall

05/24/2022, 7:07 PM
All good ☺️
3 Views