I have a very simple app, that shows a list of numbers. It is supposed to mark the first visible item with a "+" inside the TextB composeable.
TextA() = Shows the index number of the list item (has a random background color).
MyCanvas() = Has no input and just draws a rectangle with a random color.
TextA() = Reads the current state of firstVisibleIndex and checks it with the given parameter, If they are the same then shows a "+" (has a random background color).
The below code works fine, but when textB reads the state and recomposes, also MyCanvas does an unnecessary recomposition!
val randomColor
get() = Color(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))
@Composable
fun App() {
val state = rememberLazyListState()
val list = remember { (1..100).toList() }
val firstVisibleItemIndex = remember(state) {
derivedStateOf {state.firstVisibleItemIndex}
}
LazyColumn(
modifier = Modifier,state = state
) {
items(list.size) {
Row {
TextA(it)
MyCanvas()
TextB(it,firstVisibleItemIndex)
}
}
}
}
TextA() :
@Stable
@Composable
fun TextA(i: Int) {
Text(
modifier= Modifier.size(80.dp,60.dp)
.background(color = randomColor),
text = "Index:$i")
}
TextB() :
@Stable
@Composable
fun TextB(index: Int, firstVisibleItemIndex: State<Int>) {
val isFirstItem = remember {
derivedStateOf {
index==firstVisibleItemIndex.value
}
}
Text(
modifier= Modifier.size(80.dp,60.dp)
.background(color = randomColor),
text = if (isFirstItem.value) "+" else ""
)
}
MyCanvas() : // problem is here
@Stable
@Composable
fun RowScope.MyCanvas() {
Canvas(modifier =Modifier.weight(1f).height(60.dp)
.drawWithCache {
onDrawBehind {
drawRect(randomColor)
}
}
){}
}
And the result is this :
I search the stackoverflow and find this great answer by @Thracian. I tried it, and unfortunately, this is not the solution to my problem, how should I prevent this behavior?


MyCanvasis detecting overdraw, not over-composition. TheonDrawBehindis called during drawing.To avoid overdraw introduce a
graphicsLayer()which notifies Compose that draw should be independent of the rest of the composition. For example, changingMyCanvasto:will not draw when the text changes in the last column. Without a graphics layer the the two functions will share the same graphics layer which means that when one changes they both get redrawn. Normally the draw is not the bottleneck so sharing draw resources like this has better performance. However, there are cases where this is not true and you should use
graphicsLayer()when it isn't.If you want to detect over composition you can use
background()as you did forTextBor generate the random color outside theonDrawBehindlambda such as,This generates a new random color when ever there is composition but not when there is a draw. It is still overdrawing but it is always drawing the same color until it is recomposed which is unlikely as it doesn't read mutable state.
Note also that
@Stableis not necessary on these functions and is only meaningful on functions that return a value as it declares that, given a static parameter (i.e. will never change), the function's result is also static. This is the reasonInt.dpis marked as stable. If theIntthatdpis applied to is static, the result is also static. For example,60will never change as it is a literal, therefore, sincedpis marked@Stable, the compiler knows60.dpwill also never change. This is used by the compiler to avoid storing and comparing values that will never change.For
Unitreturning functions, this is a pointless annotation.