Consider this minimal code snippet (in Kotlin):
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import java.time.LocalDateTime
import java.util.*
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var time by remember {
mutableStateOf("time")
}
Column(modifier = Modifier.clickable { time = LocalDateTime.now().toString() }) {
Text(text = UUID.randomUUID().toString())
Text(text = time)
}
}
}
}
Examining the above code, from a logical perspective, one expects that upon clicking the Column
, since only the parameter time
changes, only the lower time Text
composable would be redrawn. This is because recomposition skips as much as possible.
However, one finds that the upper Text
composable is being redrawn too (the displayed UUID keeps changing).
Notice that the non-idempotence of my Column
composable should have no bearing unless the redraw is dumb.
You can try running this block of code
@Composable
fun IdempotenceTest() {
var time by remember {
mutableStateOf("time")
}
Column(
modifier = Modifier.clickable {
time = LocalDateTime.now().toString()
}
) {
Text(text = getRandomUuid())
TestComposable(text = returnSameValue())
Text(text = time)
}
}
@Composable
fun TestComposable(text: String) {
SideEffect {
Log.d(TAG, "TestComposable composed with: $text")
}
Text(text = text)
}
private fun getRandomUuid(): String {
Log.d(TAG, "getRandomUuid: called")
return UUID.randomUUID().toString()
}
private fun returnSameValue(): String {
Log.d(TAG, "returnSameValue: called")
return "test"
}
If you check the logs, you will see that everytime the state changes, the compiler tries to re-call the smallest enclosing lamda/function where the state's value is being read. Hence, the IdempotenceTest function (in my example and setContent{} lamda in yours) will get re-executed which will call the getRandomUuid
and returnSameValue
function and based on the value returned by these, it will decide whether or not to re-compose the elements which depend on these return values. If you want to prevent a calculation from happening again and again, wrap it in a remember{}
block.
Now if you were to use a Button
for example instead of a Column, you would have seen that only the content lamda of the same would have been executed. The reason why this happens is that Column is an inline function while Button in itself uses Surface inside it which is non-inline. Therefore the content of Column{}
is copied inside the enclosing function block thus leading to the entire IdempotenceTest
being invalidated for re-composition.
As a side-note, the Composable functions must be side-effect free to ensure idempotence. You can read more here.
To read more about recomposition scope, you can refer to the blog posts here and here.