Official article about Jetpack Compose phases and this article about deferring reads for performance suggests moving any state reads from Composition phase to Layout or Draw phases such as
Modifier.offset() which is read in Composition phase to
Modifier.offset{} which is read in Layout phase or
Modifier.background() to Modifier.drawBehind{} to defer read to Draw phase.
Based on the articles i created 2 separate samples.
For this created a Box that changes title color on each recomposition
@Composable
private fun MyBox(modifier: Modifier, title: String) {
LogCompositions(msg = " MyBox() COMPOSITION $title")
Column(modifier) {
// This Text changes color on every recomposition
Text(
text = title,
modifier = Modifier
.background(getRandomColor())
.fillMaxWidth()
.padding(2.dp)
)
Text(
text = "modifier hash: ${modifier.hashCode()}\n"
+ "Modifier: $modifier",
color = Color.White,
modifier = Modifier.heightIn(max = 120.dp),
fontSize = 10.sp
)
}
}
function for generating color
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
Log recompositions
class Ref(var value: Int)
// Note the inline function below which ensures that this function is essentially
// copied at the call site to ensure that its logging only recompositions from the
// original call site.
@Composable
inline fun LogCompositions(msg: String) {
val ref = remember { Ref(0) }
SideEffect { ref.value++ }
println("$msg, count: ${ref.value}")
}
First Sample
@Composable
private fun PhasesSample1() {
LogCompositions(msg = "1️⃣ PhasesSample1")
var offsetX by remember { mutableStateOf(0f) }
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "OffsetX")
Spacer(modifier = Modifier.width(5.dp))
Slider(value = offsetX,
valueRange = 0f..50f,
onValueChange = {
offsetX = it
}
)
}
val modifier1 = Modifier
// Reads value directly
.offset(x = offsetX.dp)
.layout { measurable, constraints ->
val placeable: Placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
println("️ modifier1 LAYOUT")
placeable.placeRelative(0, 0)
}
}
.background(Blue400)
.drawWithContent {
println(" modifier1 DRAW")
drawContent()
}
val modifier2 = Modifier
// Deferring state to Layout phase prevents
// Composables that have this modifier to be recomposed
.offset {
val newX = offsetX.dp.roundToPx()
IntOffset(newX, 0)
}
.layout { measurable, constraints ->
val placeable: Placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
println(" modifier2 LAYOUT")
placeable.placeRelative(0, 0)
}
}
.background(Blue400)
.drawWithContent {
println(" modifier2 DRAW")
drawContent()
}
MyBox(modifier = modifier1, "modifier1")
Spacer(modifier = Modifier.height(8.dp))
MyBox(modifier = modifier2, "modifier2")
}
This one works as expected. When i change offset with slider modifier1 goes through
Composition->Layout->Draw phases since it has Modifier.offset(x = offsetX.dp)
modifier2 calls Layout phase only, i thought it should call Draw too but it's not a major question, probably because it reads same color so it doesn't update Draw phase
Other sample is
@Composable
private fun PhasesSample2() {
LogCompositions(msg = "2️⃣ PhasesSample2")
// This state is for triggering recomposition for PhasesSample2,
// child composables don't read this state
var someValue by remember { mutableStateOf(0f) }
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "someValue")
Spacer(modifier = Modifier.width(5.dp))
Slider(value = someValue,
valueRange = 0f..50f,
onValueChange = {
someValue = it
}
)
}
val modifier3 = Modifier
.layout { measurable, constraints ->
val placeable: Placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
println(" modifier3 LAYOUT")
placeable.placeRelative(0, 0)
}
}
.drawWithContent {
println(" modifier3 DRAW")
drawRect(getRandomColor())
drawContent()
}
val modifier4 = Modifier
.layout { measurable, constraints ->
val placeable: Placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
println("⚾️ modifier4 LAYOUT")
placeable.placeRelative(0, 0)
}
}
.drawWithContent {
println(" modifier4 DRAW")
drawContent()
}
// reading color causes Composition->Layout->Draw
.background(getRandomColor())
MyBox(modifier = modifier3, "modifier3")
Spacer(modifier = Modifier.height(8.dp))
MyBox(modifier = modifier4, "modifier4")
}
Both Sample Composables are in a Column
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
PhasesSample1()
Spacer(modifier =Modifier.height(20.dp))
PhasesSample2()
}
If i move Slider in PhasesSample1 Composable log is as
I/System.out: 1️⃣ PhasesSample1, recomposition: 1
I/System.out: MyBox() COMPOSITION modifier1, recomposition: 1
I/System.out: ️ modifier1 LAYOUT
I/System.out: modifier2 LAYOUT
I/System.out: modifier1 DRAW
I/System.out: modifier3 DRAW
I/System.out: modifier4 DRAW
I/ViewRootImpl@159ca9[MainActivity]: ViewPostIme pointer 0
I/System.out: 1️⃣ PhasesSample1, recomposition: 2
I/System.out: MyBox() COMPOSITION modifier1, recomposition: 2
I/System.out: ️ modifier1 LAYOUT
I/System.out: modifier2 LAYOUT
I/System.out: modifier1 DRAW
I/System.out: modifier3 DRAW
I/System.out: modifier4 DRAW
Question
Why DRAW phases of modifier3 and modifier4 are called when they are in a different Composable with scope? As you can see only PhasesSample1 is recomposed.
If i move Slider in PhasesSample2 Composable
I/System.out: 2️⃣ PhasesSample2, recomposition: 1
I/System.out: MyBox() COMPOSITION modifier4, recomposition: 1
I/System.out: ⚾️ modifier4 LAYOUT
I/System.out: modifier1 DRAW
I/System.out: modifier3 DRAW
I/System.out: modifier4 DRAW
I/System.out: 2️⃣ PhasesSample2, recomposition: 2
I/System.out: MyBox() COMPOSITION modifier4, recomposition: 2
I/System.out: ⚾️ modifier4 LAYOUT
I/System.out: modifier1 DRAW
I/System.out: modifier3 DRAW
I/System.out: modifier4 DRAW
This time DRAW phase is invoked only modifier1 is called but not modifier2?
This is how visually changes happen. Change of color of title, section with modifierX on top, is due to recomposition also Modifier hash code changes.
Background change when title doesn't change is due to calling DRAW state as you can verify from logs.
