50

What is the difference between these two approaches?

  1. val result = remember(key1, key2) { computeIt(key1, key2) } (Docs)
  2. val result by remember { derivedStateOf { computeIt(key1, key2) } } (Docs)

Both avoid re-computation if neither key1 nor key2 has changed . The second also avoids re-computations if downstream states are derived, but else, they are identical in their behavior, aren't they?

Felix D.
  • 786
  • 1
  • 11
  • 17
  • Could it be that the keys to `remember()` do not have to be `State`s, while for `derivedStateOf()` it has to be states in order to work correctly? – Felix D. Nov 28 '21 at 14:27

4 Answers4

52

derivedStateOf {} is used when your state or key is changing more than you want to update your UI. It acts as a buffer for you, buffering out the changes you don't need. That is the primary difference between a keyed remember {} and derivedStateOf {}. With remember {}, you will still recompose as much as your key changes, which isn't a bad thing if that's what you need. If you don't need those recompositions though, that is where derivedStateOf {} comes in.

Take for example, showing a button only if the user has scrolled a LazyColumn.

val isVisible = lazyListState.firstVisibleItemIndex > 0

firstVisibleItemIndex will change 0, 1, 2 etc. as the user scrolls and cause a recomposition for every time it changes.

I only care about if it's 0 or not 0 and only want to recompose when that condition changes.

derivedStateOf is my buffer, it's buffering out all of those extra state changes I don't need and limiting the recomposition to only when the derivedStateOf changes.

val isVisible = remember { derivedStateOf { lazyListState.firstVisibleItemIndex > 0 } }

For the example given in the question, a remember(key1, key2) {} is the correct API to use there, not derivedStateOf {} because you want your UI to update any time the key changes, there isn't any change to buffer out.

Update: There is a detailed explanation of derivedStateOf in this talk https://www.youtube.com/watch?v=ahXLwg2JYpc&t=948s

Rafiul
  • 1,560
  • 7
  • 19
Ben Trengrove
  • 8,191
  • 3
  • 40
  • 58
  • In your example, what exactly does the derived state buffer? What is different about `remember { lls.fvii > 0 }`? Is the difference that the value of the Boolean expression is evaluated often, but it changes infrequently? So the derived state really only changes when the value/keys change, while `remember` evaluates the expression every time and triggers recomposition regardless of value change? – Erik Oct 19 '22 at 21:14
  • 3
    Compose recomposes when the State it is reading changes. Without derivedStateOf, the State object you will be reading is lazyListState so any time it changes you will recompose. derivedStateOf creates a new State object for you that will only change when as much as lls.fvii > 0 changes so it will recompose much less. – Ben Trengrove Oct 27 '22 at 22:44
27

AFAIK there is no difference here. It's just a coincidence that both constructs are doing the same thing here in this context. But, there are differences!

The biggest one is that derivedStateOf is not composable and it does no caching on it's own (remember does). So derivedStateOf is meant for long running calculations that have to be run only if key changes. Or it can be used to merge multiple states that are not in composable (in custom class for example).

I think the exact explanation is blurred for "outsiders", we need some input from some compose team member here :). My source for the above is this one thread on slack and my own experiments

EDIT:

Today i learned another derivedStateOf usage, very important one. It can be used to limit recomposition count when using some very frequently used value for calculation.

Example:

// we have a variable scrollState: Int that gets updated every time user scrolls
// we want to update our counter for every 100 pixels scrolled.
// To avoid recomposition every single pixel change we are using derivedStateOf
val counter = remember {
    derivedStateOf {
        (scrollState / 100).roundToInt()
    }
}

// this will be recomposed only on counter change, so it will "ignore" scrollState in 99% of cases
Text(counter.toString()).

My source for that is as direct as it can be - from the author of compose runtime and the snapshot system, the Chuck Jazdzewski himself. I highly recommend watching stream with him here: https://www.youtube.com/watch?v=waJ_dklg6fU

EDIT2:

We finally have some official performance documentation with small mention of derivedStateOf. So the official purpose of derivedStateOf is to limit composition count (like in my example). sauce

Jakoss
  • 4,647
  • 2
  • 26
  • 40
  • 2
    Thanks @Jakoss! I'm also under the impression that the docs explain this a bit too blurry. – Felix D. Jan 25 '22 at 17:45
  • 2
    While I love that compose "just works" - some of the more "internals" stuff should be better documented. But I'm sure compose will get there. Team working on that seems very commited – Jakoss Jan 25 '22 at 21:14
  • @FelixD. I added another use case, pretty nice one! – Jakoss Jan 31 '22 at 10:15
  • 1
    but I guess `val counter = derivedStateOf { (scrollState / 100).roundToInt() }` should be wrapped in `remember`: `val counter = remember { derivedStateOf { (scrollState / 100).roundToInt() } }` should be – user924 Mar 25 '22 at 09:50
  • 1
    @user924 No, the whole point is that it does not have to be. The calculations will be performed on EVERY recomposition, but the result will trigger recomposition only if it changes – Jakoss Mar 25 '22 at 10:47
  • @Jakoss derivedStateOf will produce a new State object on every recomposition without remember, whats the point of this? It must be wrapped with remember. In such case the calculation in derivedStateOf will be executed every time scrollState changed and computed value will be assigned to remembered State (counter in your case). Recomposition will be triggered when computed value is different from previous one. Recomposition of composable which reads the State value. – Roman Y Apr 07 '22 at 20:38
  • 1
    @RomanY Will it produce a new State object? I saw many examples of using `derivedStateOf` without `remember` by googlers and they never mentioned such issue – Jakoss Apr 08 '22 at 07:18
  • @RomanY i found on official docs they indeed use the remember in such case: https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#derivedStateOf(kotlin.Function0) . That's the issue with the `derivedStateOf`. Lack of proper official description and common usages – Jakoss Apr 08 '22 at 07:22
  • But if you check by yourself the following example you will see that remember can reduce recompositions count in the same way as you mention only derivedStateOf must do. Sorry, as I see no difference in ability to reduce underlying recompositions. Simple Test: https://gist.github.com/Andrew0000/b5e033c7eca3d5c5c3722e1ac191c590 – Andrey-r Dec 06 '22 at 20:09
2

val result = remember(key1, key2) { computeIt(key1, key2) } re-calculates when key1 or key2 changes but derivedStateOf is for tracking a change in one or more State/MutableState as stated in documents as

  var a by remember { mutableStateOf(0) }
    var b by remember { mutableStateOf(0) }
    val sum = remember { derivedStateOf { a + b } }
    // Changing either a or b will cause CountDisplay to recompose but not trigger Example
    // to recompose.
    CountDisplay(sum)

It's convenient to use derivedStateOf when you need to track a change in property of a State object. The value you store in State can be an object but when you need to track one or some properties of object you need to use derivedStateOf. And if it's not derived from a State/MutableState or object with an interface with @Stable annotation Composable won't recompose since recomposition requires a state change.

For instance an Input layout or number of items that you need to trigger another recomposition after a certain threshold or state.

var numberOfItems by remember {
    mutableStateOf(0)
}

// Use derivedStateOf when a certain state is calculated or derived from other state objects.
    // Using this function guarantees that the calculation will only occur whenever one
    // of the states used in the calculation changes.
    val derivedStateMax by remember {
        derivedStateOf {
            numberOfItems > 5
        }
    }


Column(modifier = Modifier.padding(horizontal = 8.dp)) {

    Row(verticalAlignment = Alignment.CenterVertically) {
        Text(text = "Amount to buy: $numberOfItems", modifier = Modifier.weight(1f))
        IconButton(onClick = { numberOfItems++ }) {
            Icon(imageVector = Icons.Default.Add, contentDescription = "add")
        }
        Spacer(modifier = Modifier.width(4.dp))
        IconButton(onClick = { if (derivedStateMin) numberOfItems-- }) {
            Icon(imageVector = Icons.Default.Remove, contentDescription = "remove")
        }
    }

 
    if (derivedStateMax) {
        Text("You cannot buy more than 5 items", color = Color(0xffE53935))
    }
}

This is whatsapp text input that displays icons based on whether text is empty or not by reading from text

internal fun ChatInput(modifier: Modifier = Modifier, onMessageChange: (String) -> Unit) {

    var input by remember { mutableStateOf(TextFieldValue("")) }
    val textEmpty: Boolean by derivedStateOf { input.text.isEmpty() }

    Row(
        modifier = modifier
            .padding(horizontal = 8.dp, vertical = 6.dp)
            .fillMaxWidth(),
        verticalAlignment = Alignment.Bottom
    ) {

        ChatTextField(
            modifier = modifier.weight(1f),
            input = input,
            empty = textEmpty,
            onValueChange = {
                input = it
            }
        )

        Spacer(modifier = Modifier.width(6.dp))

        FloatingActionButton(
            modifier = Modifier.size(48.dp),
            backgroundColor = Color(0xff00897B),
            onClick = {
                if (!textEmpty) {
                    onMessageChange(input.text)
                    input = TextFieldValue("")
                }
            }
        ) {
            Icon(
                tint = Color.White,
                imageVector = if (textEmpty) Icons.Filled.Mic else Icons.Filled.Send,
                contentDescription = null
            )
        }
    }
}

enter image description here

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • can you please check this example https://stackoverflow.com/questions/71614556/when-to-use-derivedstateof-in-jetpack-compose – user924 Mar 25 '22 at 09:48
0

I've just tested this snippet of code with derivedStateOf and remember with the key. In general, I'd say it's the same (at least in terms of functionality you want to achieve), but LayoutInspector showed some differences:

@Composable
fun CounterButton() {
    val clicks = remember { mutableStateOf(0) }
    val counter = remember(clicks.value / 5) { mutableStateOf(clicks.value / 5) }

    Button(
        onClick = { clicks.value++ }
    ) {
        Text("${counter.value} clicked")
    }
}

enter image description here

@Composable
fun CounterButton() {
    val clicks = remember { mutableStateOf(0) }
    val counter = remember { derivedStateOf { clicks.value / 5 } }

    Button(
        onClick = { clicks.value++ }
    ) {
        Text("${counter.value} clicked")
    }
}

enter image description here

So, in first case, with remember with the key on every button click we trigger recomposition of the Button and for some reason for the CounterButton. On every 5th click we trigger a recomposition for a Text as well.

In second case, with derivedStateOf the behavior is the same, except we do not trigger any recomposition for CounterButton.

Oleg Golomoz
  • 502
  • 4
  • 12