Bites of Compose 4
Learn Jetpack Compose one bite at a time
This time we are focusing on derivedStateOf
and how it is different from remember
.
Let’s look at this simple example:
Situation 1
What will happen when user clicks on the Text
?
@Composable
private fun Situation1() {
var name by remember {
mutableStateOf("guowei")
}
val uppercase by remember {
derivedStateOf { name.uppercase() }
}
Text(uppercase, modifier = Modifier.clickable { name = "hello" })
}
Answer
The text will change from “GUOWEI” to “HELLO”.
This is because the
uppercase
state is listening to changes of another statename
. So whenname
changes,uppercase
will also change accordingly. This is the basic usage ofderivedStateOf
.
Situation 2
But can we achieve the same behaviour without using derivedStateOf
? Take a look at the following code, what will happen if the user clicks on the text?
@Composable
private fun Situation2() {
var name by remember {
mutableStateOf("guowei")
}
val uppercase = remember(name) {
name.uppercase()
}
Text(uppercase, modifier = Modifier.clickable { name = "hello" })
}
Answer
Text will change to “HELLO”, the same as in Situation1. Hmm, does this mean
derivedStateOf
andremember()
are interchangeable? Let’s explore more.
Situation 3
Let’s change from name
to names
. What will happen if the user clicks on the names Column?
@Composable
private fun Situation3() {
val names = remember {
mutableStateListOf("Bob", "Jane")
}
val uppercaseNames by remember {
derivedStateOf { names.map { it.uppercase() } }
}
Column(modifier = Modifier.clickable { names.add("hello") }) {
uppercaseNames.forEach {
Text(it)
}
}
}
Answer
There will be a “HELLO” appearing at the end of the Column. So basically
derivedStateOf
still works fine.
Situation 4
Let’s now see if the remember
version still works.
What happens if the user clicks the Column?
@Composable
fun Situation4() {
val names = remember {
mutableStateListOf("Bob", "Jane")
}
val uppercaseNames = remember(names) {
names.map { it.uppercase() }
}
Column(modifier = Modifier.clickable { names.add("hello") }) {
uppercaseNames.forEach {
Text(it)
}
}
}
Answer
Nothing! But, why? Clicking the Column will add the new “hello” into
names
, no problem with that. But sincenames
is pointing the same object in memory,remember(names)
will think nothing has changed, it comparesnames
to itself.(well, if instead of adding a new element we assign a new object tonames
, then that is another story). So if you useremember
onInt
orString
, there is no problem, because the only way to change them is to assign a new value. But withList
orMap
, there is a problem,remember
only works if you assign a new value, but NOT when the object itself changed from imside. OK, now seems thatderivedStateOf
can apply to more cases thanremember
, but is there a case where onlyremember
will work? Let’s see the next example.
Situation 5
What will happen if user clicks on the text?
@Composable
fun Situation5() {
var name by remember {
mutableStateOf("guowei")
}
UpperCasedName(name) { name = "hello" }
}
@Composable
fun UpperCasedName(name: String, onClick: () -> Unit) {
val upperCased = remember(name) { name.uppercase() }
Text(upperCased, modifier = Modifier.clickable { onClick() })
}
Answer
The text will change to “hello”. Let’s go through what has happened in this case:
onClick()
callback is called.name = "hello"
executed.- Trigger recompose.
UpperCasedName("hello")
is called.- Inside
UpperCasedName()
,remember(name)
sees that name has changed from “guowei” to “hello”, so the code inside it will be executed again,upperCased
will be “HELLO”.- Recompose of
Text
.
Situation 6
Now, let’s see the derivedStateOf
version will also work. What will happen if user clicks the text this time?
@Composable
fun Situation6() {
var name by remember {
mutableStateOf("guowei")
}
UpperCasedName(name) { name = "hello" }
}
@Composable
fun UpperCasedName(name: String, onClick: () -> Unit) {
val upperCased by remember {
derivedStateOf { name.uppercase() }
}
Text(upperCased, modifier = Modifier.clickable { onClick() })
}
Answer
Nothing will happen. Let’s go through what has happened here also.
1 - 4: The same as in Situation 5.
- Since
name
is just a string, and theremember
insideUpperCasedName
has no parameter, soupperCased
will get the remembered value. So nothing will change in this case.
Seems that the
observe chain
is cut off by passing thename
as a parameter into another function. This is because we usedby
,andname
is a deligated variable. So on the calling side this is the same asUpperCasedName("hello") { name = "hello" }
. OK, then is there a way to keep the observe chain inside the other function? what if we pass the state object itself?
Situation 7
What will happen if user click the text?
@Composable
fun Situation7() {
val name = remember {
mutableStateOf("guowei")
}
UpperCasedName(name) { name.value = "hello" }
}
@Composable
fun UpperCasedName(name: State<String>, onClick: () -> Unit) {
val upperCased by remember {
derivedStateOf { name.value.uppercase() }
}
Text(upperCased, modifier = Modifier.clickable { onClick() })
}
Answer
“GUOWEI” will change to “HELLO”. This time the State object is passed to the other function, so the obervation is still preserved. But, we normally wouldn’t use this way. Because it is too restricting to only allow a State of String to be passed to the function. So the reusability is poor.
Situation 8
Let’s change things a bit again, what will happen if user clicks on the Column?
@Composable
fun Situation8() {
val names = remember {
mutableStateListOf("Bob", "Jane")
}
UpperCasedName(names) { names.add("hello") }
}
@Composable
fun UpperCasedName(names: List<String>, onClick: () -> Unit) {
val upperCaseNames by remember {
derivedStateOf { names.map { it.uppercase() } }
}
Column(modifier = Modifier.clickable { onClick() }) {
upperCaseNames.forEach {
Text(it)
}
}
}
Answer
“HELLO” will be appended to the list of names. The key to understand this is to understand what is passed to the function
UpperCasedName
.names
is aMutableList
object, and becauseMutableList<E> : List<E>
, so it can be directly passed to theUpperCasedName()
function. So even though the function’s parameter is defined asList<String>
, we can actually pass in a state object likeMutableList<String>
.
Situation 9
Let’s again change things a little and see if things are still working.
@Composable
fun Situation9() {
var names by remember {
mutableStateOf(listOf("Bob", "Jane"))
}
UpperCasedName(names) { names = mutableListOf("hello", "kitty") }
}
@Composable
fun UpperCasedName(names: List<String>, onClick: () -> Unit) {
val upperCaseNames by remember {
derivedStateOf { names.map { it.uppercase() } }
}
Column(modifier = Modifier.clickable { onClick() }) {
upperCaseNames.forEach {
Text(it)
}
}
}
Answer
Nothing will change. Till now, can you figure out why and how to fix? OK, the fix is to use both parameterized remember and derivedStateOf together.
@Composable
fun UpperCasedName(names: List<String>, onClick: () -> Unit) {
val upperCaseNames by remember(names) {
derivedStateOf { names.map { it.uppercase() } }
}
Column(modifier = Modifier.clickable { onClick() }) {
upperCaseNames.forEach {
Text(it)
}
}
}
So this covers both cases:
- If the
names
changes to a totally different object, the parameterizedremember
will cover it.- If the
names
is a State object and its internal elements change, then thederivedStateOf
will cover it.
Share this post
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Email