Bites of Compose 2
Learn Jetpack Compose one bite at a time
Situation 1
Take a look at the Composable below, what will happen if the button is clicked?
@Composable
fun Situation1() {
val names by remember { mutableStateOf(mutableListOf("Bob", "Tom")) }
Column {
names.forEach {
Text(it)
}
Button(onClick = { names.add("Jane") }) {
Text("Add Jane!")
}
}
}
Answer
The list will still consist of Bob and Tom. Jane will not be added. The reason is that for mutable states, only the assignment operation is “observed”. Since adding an element to a list is not an assignment, recompose will not happen.
Situation 2
How about this time?
@Composable
fun Situation2() {
val names = remember { mutableStateListOf("Bob", "Tom") }
Column {
names.forEach {
Text(it)
}
Button(onClick = { names.add("Jane") }) {
Text("Add Jane!")
}
}
}
Answer
It is working correctly this time, Jane is added to the view. This way, changes inside the list (add, delete items) will be correctly “observed”.
Situation 3
Will the UserPage
composable get recomposed after the button is clicked?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val user = User("Guowei Lv")
setContent {
var flag by remember {
mutableStateOf(true)
}
Column {
Text(text = "Flag is $flag")
UserPage(user = user)
Button(onClick = { flag = !flag }) {
Text("Click me!")
}
}
}
}
}
@Composable
fun UserPage(user: User) {
println("User page recomposed!")
Text("${user.name}'s page")
}
Answer
No. “User page recomposed!” message is only printed when we launch the app, but clicking the button will not trigger the recompose of
UserPage
.
Well, this kind of makes sense, the
user
object has not changed, so there is no reason to do a recompose, right?
Situation 4
Please pay attention to what is changed, and will UserPage
recompose this time after clicking the button?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val user = User("Guowei Lv")
setContent {
var flag by remember {
mutableStateOf(true)
}
Column {
Text(text = "Flag is $flag")
UserPage(user = user)
Button(onClick = {
flag = !flag
user = User("Guowei Lv") // assign a new User object with the same name
}) {
Text("Click me!")
}
}
}
}
}
Answer
The answer is still no. So this proves that Compose uses “structural equality” to test whether a recompose is needed. Well, really?
Situation 5
Let’s make one character change, val
-> var
of name
in User
. Will things change now?
data class User(var name: String)
// ...
// rest of code same as Situation 4
// ...
Answer
Suprisingly, this one character change will leads to the recomposition of UserPage
every time the button is clicked.
This is because now User
is seen as “unreliable” data type by Compose.
So the rule now is that:
- For reliable data types, Compose use structural equality for testing if there is need for recompose.
- For unreliable data types, always recompose no matter what.
OK, I know, these all sound magical. Don’t worry, I will explain in more detail.
Situation 5
Let me now try to explain why by changing name
property from val
to var
, the UserPage
will always be recomposed.
I abstract away some implementation details of previous example and now it becomes the following:
data class User(var name: String)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val user1 = User("Guowei Lv")
val user2 = User("Guowei Lv")
var user = user1
setContent {
// ...
UserPage(user = user)
// ...
// ... Event A
user = user2
// ...
// ... Event B
// user2.name = "Hello Kitty"
}
}
}
@Composable
fun UserPage(user: User) {
Text("${user.name}'s page")
// ... Event C
// trigger recompose of UserPage from within
// ...
}
Somethings to note here:
- There are two structurally equal objects
user1
anduser2
. - Component
UserPage
is passed the objectuser
, initiallyuser
is assigned withuser1
.
With this setup, let’s imagine that somehow, user
gets assigned to user2
(Event A) and a recompose is triggered inside Situation5
component.
In such case, compose will need to decide whether UserPage
will need a recompose or not. Well, we already know that a recompose will be triggered,
even though user1
and user2
are structurally equivalent.
Let’s think from the other way around, what if compose uses structural equality here and skips a recompose, let’s see if some bad things will happen.
If the recompose is skipped, we are left with the situation:
user
is pointing touser2
.UserPage
component is still bound touser1
, because recompose is skipped (this is the key).
OK, all set. Now, let’s imagine that 2 events happen:
- Event B:
user2
´sname
property changes to “Hello Kitty”. - Event C: a recompose is triggered somehow from within
EventPage
.
With these two events happen, we would expect that EventPage
will use the new name
from user2
, right?
Well, that will not happen, because UserPage
is still hooked up with user1
, remember?
In other words, if we pass o1
to Component
and later pass it o2
, we can only skip o2
if we can guarantee that o1
and o2
will forever be equal. If not, we’d better replace o1
with o2
, because at a later point o2
might change to be different from o1
and Component
needs to know it.
OK, that’s it for now, there are more to discuss about this topic, so stay tuned!
Share this post
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Email