I'm creating a custom slider control for my app but I can't avoid unnecessary recomposition without adding some ugly hacks...
CustomSlider1
is a component that recomposes all its children when the value changes; CustomSlider2
is what I came up with that does not... but the code doesn't seem right, so could anyone tell me what I'm doing wrong in CustomSlider1
and if CustomSlider2
is indeed correct?
The difference between the 2 components is basically that I read the value through a lambda and also added the Slider
component inside a Scoped composable.
I'm using recomposeHighlighter to show recompositions.
Here's a gif showing how both behaves when I change its value:
Here's the code:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestTheme {
Column {
var value by remember {
mutableStateOf(50f)
}
CustomSlider1("Custom Slider", value, 50f, true, { value = it }, 0f..100f, 5)
Spacer(modifier = Modifier.padding(10.dp))
CustomSlider2("Custom Slider 2", { value }, 50f, true, { value = it }, 0f..100f, 5)
}
}
}
}
}
@Composable
fun CustomSlider1(
label: String,
value: Float,
defaultValue: Float,
enabled: Boolean = true,
onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int = 0,
) {
Column(
modifier = Modifier.recomposeHighlighter()
) {
Text(
text = label,
color = if (enabled) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.5f),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.recomposeHighlighter()
)
Row {
Slider(
value = value,
valueRange = valueRange,
steps = steps,
enabled = enabled,
onValueChange = onValueChange,
modifier = Modifier
.recomposeHighlighter()
.weight(1f)
)
IconButton(
onClick = { onValueChange(defaultValue) },
enabled = enabled,
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.primary),
modifier = Modifier.recomposeHighlighter()
) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
modifier = Modifier.recomposeHighlighter()
)
}
}
}
}
@Composable
fun CustomSlider2(
label: String,
value: () -> Float,
defaultValue: Float,
enabled: Boolean = true,
onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int = 0,
) {
Column(
modifier = Modifier.recomposeHighlighter()
) {
Text(
text = label,
color = if (enabled) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.5f),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.recomposeHighlighter()
)
Row {
Scoped { //had to do this to avoid recompositions...
Slider(
value = value.invoke(),
valueRange = valueRange,
steps = steps,
enabled = enabled,
onValueChange = onValueChange,
modifier = Modifier
.recomposeHighlighter()
.weight(1f)
)
}
IconButton(
onClick = { onValueChange(defaultValue) },
enabled = enabled,
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.primary),
modifier = Modifier.recomposeHighlighter()
) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
modifier = Modifier.recomposeHighlighter()
)
}
}
}
}
@Composable
fun Scoped(content: @Composable () -> Unit) = content()