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.