Can anyone help me understand an example from the ...
# compose
d
Can anyone help me understand an example from the docs explaining the usage of
derivedStateOf
?
Copy code
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
    val todoTasks = remember { mutableStateListOf<String>() }
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf {
            todoTasks.filter { task ->
                highPriorityKeywords.any { keyword ->
                    task.contains(keyword)
                }
            }
        }
    }

    // ...The UI
    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}
The idea is to recompose only when
todoTasks
or
highPriorityKeywords
change. Questions: 1. Why does the
remember
function accept keys here, if the
derivedStateOf
will update automatically whenever one of the states changes? And it only accepts one —
highPriorityKeywords
, why do then we not pass
todoTasks
as well? 2. How would the usage of
derivedStateOf
instead of
remember(todoTasks, highPriorityKeywords)
optimize the performance? Wouldn't these two things do the same stuff in this particular example? I understand the difference between
remember+keys
and
remember+derivedStateOf
. But in this example we use
remember+keys+derivedStateOf
, which really bothers me.
s
1) highPriorityKeywords is not a value backed by snapshot state, aka it's just a normal object, so derivedStateOf would have no way to observe changes done to it automatically. So you pass it as a key, so that whenever that changes, you restart the entire derivedStateOf block. todoTasks is in fact a MutableState so the derivedStateOf can see that it reads a value from snapshot state, and registers as a listener so to speak, so whenever the state changes internally, derivedStateOf is informed about the state and re-runs the lambda, without restarting the entire derivedStateOf initialization this time. Then if the result of the lambda results in a different value compared to the old one, then it will update the resulting state. 2) it comes from what I described in 1). If the MutableState changes, only the little lambda in there is rerun, and if the resulting value is not changed, it simply doesn't do anything, the resulting state does not change so no new recompositions etc. If you were to pass the item as a key to the lambda you would: • Now be reading the state object in the outside scope to see if it has changed, this means that it invalidates the current recomposition scope you're in, potentially resulting in more unnecessary recompositions if in the same scope you got other composables that are not skippable for example. • If todoTasks does actually change, the entire remember block is rerun, and I think with it more work is done (to save the value to the slot table etc) vs just rerunning the lambda inside an already setup derivedStateOf. That's my understanding of this and why derivedStateOf is good and how to use it properly. If you scour in this channel for even more discussions on this you'll see a lot of messages explaining more around this topic.
tl;dr:
remember + keys + derivedStateOf
is in fact necessary if you're planning to create a new state coming from a mix of other state objects, and other non-state objects (which derivedStateOf wouldn't know how to observe like it does state objects)
d
I think I got it. If we use something like:
Copy code
val highPriorityTasks = remember(todoTasks, highPriorityKeywords) {
    todoTasks.filter { task ->
        highPriorityKeywords.any { keyword ->
            task.contains(keyword)
        }
    }
}
And then, for example, add 10 tasks that do not contain high priority keywords, we will end up with 10 unnecessary recompositions that could be avoided using
derivedStateOf
. And we use
highPriorityKeywords
as a key simply because its not a state. Thank you, @Stylianos Gakis!
e
It's simple, when your logic function is 1-1 should use remember(key) else derivedStateOf to cache result and prevent unnecessary recomposition
s
Daniel: Technically, you'd end up with unnecessary state reads in the current recomposition scope, which would try to recompose other composables in the same scope yes. If they're all stable and their inputs didn't change either it wouldn't recompose too much, but you got the idea. I think you understood it all, so I'm very glad 😊 @Erfannj En no it clearly isn't as simple as that 😅 and I did explain partly why it's not.
❤️ 1
r
We were just talking about this example last week and why we need to change it
derivedStateOf is truly useful when the frequency of change of the input is different from that of the output
In this particular case derivedStateOf is not needed
s
Is not needed, as opposed to what, a remember with both as keys? Is it not the case that if highPriorityTasks doesn't have any other place where the state is read, and you only limit that to inside the derivedStateOf, it doesn't need to invalidate the current recomposition scope (if the list changed in a way that the resulting highPriorityTasks isn't different of course), therefore it won't maybe trigger an unnecessary recomposition to composables that may not be skippable for example? What do you mean by "is not needed"?
r
If the output of derivedStateOf changes every time highPriorityTasks changes, you get no benefit from using derivedStateOf
(and if the change ratio isn't 1:1 but close, it might also not be worth it since derivedStateOf has its own overhead)
Anyway we want to make the doc clearer with a better example
s
Yes, but here the derivedStateOf filters the list for the items that contain the keyword, if the list is altered in a way where another entry is added, but that entry doesn't have that keyword, then the resulting highPriorityTasks won't be any different, aka you get the benefits of derivedStateOf here.
Ah okay, yeah, the balance of when it's worth it vs when the rate of change is almost the same is for sure something that is very hard to depict in the docs. Also you're also right that there's definitely room for better examples. The one that made me understand derivedStateOf was definitely seeing an example with a list scroll state being observed to know when to show a "scroll to top" item when it was at scrolled at least a little bit. Hit the nail of "input changes often, output changes much less often"
r
Right but the problem is that if highPriorityTasks changes, recomposition happens and a new derivedStateOf is created which is expensive
Let me find what the author of derivedStateOf had to say about it a few days ago :)
r
"derivedStateOf is only needed in extremely rare situations, TBH, and it is quite expensive so really should be avoided unless it is providing benefit."
💯 3
thank you color 5