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?
MyCanvas
is detecting overdraw, not over-composition. The onDrawBehind
is 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, changing MyCanvas
to:
@Composable
fun RowScope.MyCanvas() {
Canvas(modifier =Modifier.weight(1f).height(60.dp)
.graphicsLayer()
.drawWithCache {
onDrawBehind {
drawRect(randomColor)
}
}
){}
}
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 for TextB
or generate the random color outside the onDrawBehind
lambda such as,
@Composable
fun RowScope.MyCanvas() {
val color = randomColor
Canvas(modifier =Modifier.weight(1f).height(60.dp)
.drawWithCache {
onDrawBehind {
drawRect(color)
}
}
){}
}
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 @Stable
is 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 reason Int.dp
is marked as stable. If the Int
that dp
is applied to is static, the result is also static. For example, 60
will never change as it is a literal, therefore, since dp
is marked @Stable
, the compiler knows 60.dp
will also never change. This is used by the compiler to avoid storing and comparing values that will never change.
For Unit
returning functions, this is a pointless annotation.