I’m trying to understand why this use of `Crossfad...
# compose
r
I’m trying to understand why this use of
Crossfade
is leaking its state holder. I have a Slider that I can interact with normally (no crossfades) but that will crossfade to its default value when a “reset” Button is pressed. I do this using a MutableState within a MutableState, the outer one being the target for Crossfade, and the inner one being the slider value (code in 🧵.) In the heap dump (after forcing garbage collection) I can see that the number of allocated SliderStateHolder/SliderState objects increases with each reset. Inasmuch as I understand heap dumps, the references look like they’re coming from “SnapshotStateList”, which would seem to indicate Compose continues to track the old states. I’d like to understand why (and what I could do to prevent it). Thanks.
Copy code
data class SliderState(
    val value: Int = 5
)

class SliderStateHolder(
    state: SliderState = SliderState()
) {
    var state by mutableStateOf(state)

    fun onValueChange(value: Int) {
        this.state = SliderState(value)
    }
}

var sliderStateHolder by mutableStateOf(SliderStateHolder())

@Composable
fun SliderWithResetCrossfade() {
    Column {
        Crossfade(
            targetState = sliderStateHolder,
            animationSpec = tween(1000)
        ) { sliderStateHolderX ->
            Slider(
                value = sliderStateHolderX.state.value.toFloat(),
                onValueChange = { sliderStateHolderX.onValueChange(it.roundToInt()) },
                valueRange = 0f..10f,
                steps = 9
            )
        }
        Button(
            modifier = Modifier.size(100.dp),
            onClick = { sliderStateHolder = SliderStateHolder() },
        ) {
            Text(text = "Reset")
        }
    }
}
d
There's a
MutableStateList
in Crossfade tracking all the states that are still active from the animation's perspective. That means during the crossfade, the old state(s) will continue to be in the MutableStateList until the content for those states has been faded out completely.
That list should be reset to only contain the target state once the animation finishes. Did you capture the heap dump after the Crossfade has finished?
r
Yes. I did two frequencies of clicks — quick ones that interrupted the animation, and ones greater than my tween time of 1 second. It did not seem to make a difference in the accumulation of objects. I just now re-ran with only clicks after the animation ended (I lowered the tween time to 50ms to make it easier). I even waited for the button ripple to end. 10 clicks gave me 11 each of sliderStateHolder and sliderState.
Taking out the Crossfade, there is no accumulation (although, while there is one copy of each state at app startup, there are always two of each state after n resets — not sure why there’d be two and not one.) Thank you for your time!
I had assumed there was something special about the “two degrees of mutable” but I just tried with a “normal”
Crossfade
and see the same problem. I took the example from the docs and modified it to put the string in an object (can you track primitives in a heap dump?). In this example,
Screen
leaks.
Copy code
data class Screen(
    val name: String
)

@Composable
fun CrossfadeLeaks() {
    var currentPage by remember { mutableStateOf(Screen("A")) }
    Column(modifier = Modifier.padding(top = 20.dp, start = 20.dp)) {
        Crossfade(targetState = currentPage) { screen ->
            when (screen) {
                Screen("A") -> Text("Page A")
                Screen("B") -> Text("Page B")
            }
        }
        Button(onClick = { currentPage = if (currentPage == Screen("A")) Screen("B") else Screen("A") }
        ) {
            Text(text = "Toggle")
        }
    }
}
Doris: I was wondering if you can replicate this or otherwise confirm that it is an issue. I’m trying to gauge the severity of this in my app (I have several other elements within the scope of the Crossfade that have bigger/deeper state objects), and whether I should look for an alternative to Crossfade. (I also don’t know if this is a debug only thing, in which case it wouldn’t matter. )
Thank you so much. I don’t know Compose internals well enough to know the role of
SmallPersistentVector
, but I’ll assume it’s something indirectly related to
Crossfade
. I will post the issue here when I write it up.