I'm not entirely sure how derivedStateOf works. It...
# compose
j
I'm not entirely sure how derivedStateOf works. It will not be updated more than at the initial composition when using the parameters of the function as the trigger. If I do a "rememberUpdatedState" on the parameter it works. Why is it not enough to look at the parameter as it is? Further up in the composition tree the parameter is part of a state class.
Copy code
val currentFillPercent by rememberUpdatedState(fillPercent)
val currentIndicatorCount by rememberUpdatedState(indicatorCount)

val filledIndicatorCount by remember {
    derivedStateOf {
        ((currentIndicatorCount * currentFillPercent / 100f) + 0.5f).toInt()
    }
}

val fillColor by remember {
    derivedStateOf {
        when (currentFillPercent) {
            in 0 until mediumThresholdPercentage -> colors.lowColor
            in mediumThresholdPercentage until highThresholdPercentage -> colors.mediumColor
            else -> colors.highColor
        }
    }
}
a
The main issue at play here is the lambda capturing parameters. Because the
remember
has no keys, the code inside will only be run once, and therefore it captures a reference to the initial value of the parameter. If the function recomposes, and there is a new value of the parameter, it won’t capture the new value.
So if you have this:
Copy code
@Composable
fun Repro(
    fillPercent: Float,
    indicatorCount: Int,
) {
    val filledIndicatorCount by remember {
        derivedStateOf {
            ((indicatorCount * fillPercent / 100f) + 0.5f).toInt()
        }
    }
}
then that’s equivalent to
Copy code
@Composable
fun Repro(
    fillPercent: Float,
    indicatorCount: Int,
) {
    val filledIndicatorCount by remember {
        val calculation = {
            ((indicatorCount * fillPercent / 100f) + 0.5f).toInt()
        }
        derivedStateOf(calculation)
    }
}
It’s a bit more clear that
calculation
will be initialized once, and it will capture the initial
fillPercent
and
indicatorCount
values
rememberUpdatedState
fixes that by creating a state object that’s captured, instead of the value itself. The
by
delegate hides what’s going on a bit, so it looks almost the same:
Copy code
val currentFillPercent by rememberUpdatedState(fillPercent)
val currentIndicatorCount by rememberUpdatedState(indicatorCount)

val filledIndicatorCount by remember {
    derivedStateOf {
        ((currentIndicatorCount * currentFillPercent / 100f) + 0.5f).toInt()
    }
}
is equivalent to
Copy code
val currentFillPercentState = rememberUpdatedState(fillPercent)
val currentIndicatorCountState = rememberUpdatedState(indicatorCount)

val filledIndicatorCount by remember {
    derivedStateOf {
        ((currentIndicatorCountState.value * currentFillPercentState.value / 100f) + 0.5f).toInt()
    }
}
j
What I don't quite understand though, is that underlying composables will be update when there are changes to for example the fillPercent value, I suppose since Compose further up the tree knows about the state object and can decide what to update based on that. For some reason that updating logic does not apply to other created states or callbacks?
a
Parameters changing do apply to other created states or callbacks, if they aren’t using
remember
or the
remember
has a key with the parameter. So an alternate solution would be add the parameters to the keys of `remember`:
Copy code
@Composable
fun Repro(
    fillPercent: Float,
    indicatorCount: Int,
) {
    val filledIndicatorCount by remember(indicatorCount, fillPercent) {
        derivedStateOf {
            ((indicatorCount * fillPercent / 100f) + 0.5f).toInt()
        }
    }
}
But then at that point,
derivedStateOf
is basically doing nothing since it is being recreated every time. So you might as well do
Copy code
val filledIndicatorCount = remember(indicatorCount, fillPercent) {
        ((indicatorCount * fillPercent / 100f) + 0.5f).toInt()
    }
But then there, you could also just remove the
remember
and just inline the value calculation:
Copy code
val filledIndicatorCount = ((indicatorCount * fillPercent / 100f) + 0.5f).toInt()
j
Isn't it a good idea to use derivedStateOf here since filledIndicatorCount wont actually change for every fillPercent value?
I think I understand how it works now with the state update. Compose will only re-compose one level down in the tree I think? And it's up to the next component to decide what needs to be redrawn. (for example limiting re-drawing like I'm doing now with derivedStateOf)
a
It depends on how expensive it is? Yeah, another way to say it is that Compose will skip work when it can if nothing has changed (that it knows about).
derivedStateOf
is a way to tweak how often and when work is done, which can be really useful in areas that are performance sensitive, especially if there are some inputs that change really often, but may not always change the output. But you don’t have to use it for every piece of state that’s changing
j
I see. Well thanks, I understand it a bit better now.
156 Views