Blog Infos
Author
Published
Topics
Published

derivedStateOf helps to avoid unnecessary recompositions to achieve better performance. In the story we will deep-dive into derivedStateOf API and will see how it is different from remember(key) .

We will see When/How we should use derivedStateOf . We will further see comparison between remember(key) and derivedStateOf and in the end we will look into some Applications of derivedStateOf .

We will answer some questions to understand derivedStateOf API, the list of such questions is below.

Page Content
  • When derivedStateOf API be used?
  • Why should we use remember with derivedStateOf ?
  • Can we use remember(key) (with key) as a replacement of derivedStateOf?
  • When should remember(key) (with key) be used along with derivedStateOf ?
  • When should we not use derivedStateOf and prefer remember(keys) over derivedStateOf ?
  • Applications
  • Github
When derivedStateOf API be used?

derivedStateOf{} is used when a compose state is derived from another compose state and the derived state is changing less frequently than the source state. derivedStateOf executes calculations block every time when internal state changes but the composable function will recompose only when the calculated value changes. This reduces the unnecessary recompositions making sure that composable should only recompose when it’s really required. To summarize, the following are important points.

  1. Derived state B is calculated and derived from another state A
  2. Derived State B is changing/updating less frequently than source state A
  3. Derived State B is updating UI when it changes.
usecase:

To understand better: Let’s take a use-case to show a Scroll To Top button when the user scrolls the list when the first visible index in the list is greater than 0 lazyListState.firstVisibleItemIndex > 0.

First Let’s see basic code examples without using derivedStateOf and see its impact on performance/recomposition count.

@Composable
fun ScrollToTopBtnWithoutDerivedStateOf(lazyListState: LazyListState) {
val isEnabled = lazyListState.firstVisibleItemIndex > 0
Button(onClick = { /*TODO*/ }, enabled = isEnabled) {
Text(text = "Scroll To Top")
}
}

Let’s see recomposition count for Scroll To Top button via Layout Inspector.

From Layout Inspector we can see there are a lot of recompositions for Scroll To Top Button. Every time lazyListState.firstVisibleItemIndex changes it calculates new value for isEnabled and causes recomposition for the Button.

These are all unnecessary recompositions because when firstVisibleItemIndex is more than 1 then the Button should be recomposed to set it enabled = true but when firstVisibleItemIndex goes to 2, 3, 4 or more it should not recompose the Button as the isEnabled value will not change. We can see here that the Derived State isEnabled is changing less frequently than the Source State lazyListState.firstVisibleItemIndex which is ideal case for derivedStateOf API.

Let’s now see the code example below where we are using derivedStateOf API

@Composable
fun ScrollToTopBtnWithDerivedStateOf(lazyListState: LazyListState) {
val isEnabled by remember { derivedStateOf { lazyListState.firstVisibleItemIndex > 0 } }
Button(onClick = { /*TODO*/ }, enabled = isEnabled) {
Text(text = "Scroll To Top")
}
}

In the code we are using derivedStateOf with remember to effectively use derivedStateOf API. We will see below in the story when we have to use remember with derivedStateOf .

Let’s take a look at the recomposition count for the Button in Layout Inspector.

We can see the recomposition count has decreased significantly for the Button. It only recompose when lazyListState.firstVisibleItemIndex changes to 1 the other intermediate values 2, 3 or more will calculate new value but derivedStateOf will not cause recomposition because the derived state value has not changed.

Why should we use remember with derviedStateOf?

To understand, let’s see how individual APIs work.

remember API executes its lambda the first time ( composition phase ), calculates the value and caches it for future. During recomposition it takes the cached value and uses it.

To understand derivedStateOf , Let’s look at the code Under The Hood.

fun <T> derivedStateOf(calculation: () -> T): State<T> = DerivedSnapshotState(calculation)
////
private class DerivedSnapshotState<T>(
private val calculation: () -> T
) : StateObject, DerivedState<T> {
private var first: ResultRecord<T> = ResultRecord()
/// class implementation
}
// DerivedState<T> interface
internal interface DerivedState<T> : State<T> {
/**
* The value of the derived state retrieved without triggering a notification to read observers.
*/
val currentValue: T
/**
* A list of the dependencies used to produce [value] or [currentValue].
*
* The [dependencies] list can be used to determine when a [StateObject] appears in the apply
* observer set, if the state could affect value of this derived state.
*/
val dependencies: Set<StateObject>
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

derivedStateOf executes its lambda, allocates StateObject which is storing DerivedState<T> value and observing dependencies required to change DerivedState<T> value. e.g In our example lazyListState.firstVisibleItemIndex is dependency for changing derived value inside StateObject. In future whenever dependencies change it calculates new derived value and updates the StateObject and causes recomposition only when derived value changes.

If we don’t use remember with derivedStateOf the derived StateObject created from derivedStateOf is not cached and every time the lazyListState.firstVisibleItemIndex changes it re-allocates new StateObject removing previous one causing unnecessary overhead under the hood.

We should always use remember with derivedStateOf in order to cache the allocated StateObject storing derived value and dependencies which is internally observing for change in dependencies and producing new derived state value.

Can we use remember(key) as replacement of derviedStateOf?

What if we use remember(key) replacing derivedStateOf ?

remember(key) API executes its block of code whenever the provided key value changes in order to calculate new value from calculation block.

Let’s see a code example below where we will take the same example lazyListState.firstVisibleItemIndex > 0 in order to enable the Scroll To Top Button for both cases.

In one case we are using derivedStateOf as before and in the other case we are using just remember(key) without derivedStateOf .

@Composable
fun ScrollToTopDerivedAndRememberedCase(lazyListState: LazyListState) {
val isEnabledDerivedStateCase by remember { derivedStateOf { lazyListState.firstVisibleItemIndex > 0 }}
val isEnabledRememberCase = remember(lazyListState.firstVisibleItemIndex) { lazyListState.firstVisibleItemIndex > 0}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { /*TODO*/ }, enabled = isEnabledDerivedStateCase) {
Text(text = "Derived State Button")
}
Button(onClick = { /*TODO*/ }, enabled = isEnabledRememberCase) {
Text(text = "Remembered Button")
}
}
}

Let’s see Layout Inspector to visualise the recomposition count in both cases.

In Layout Inspector the recomposition counts are exactly same for both cases, then what is the difference?

Because the input key value is changing more often than the output value, remember(key) is re-allocating new value on every key change, caching it removing old one on every firstVisibleItemIndex change which is causing overhead under the hood. So we can not use remember(key) as a replacement of derivedStateOf

When input values are changing more than the output value changes then we should not use remember(key) as it causes extra overhead rather we should use derivedStateOf which is an ideal choice in such cases.

When should remember(key) be used along with derivedStateOf?

There are cases where remember(key) with key must be used with derivedStateOf .

Let’s take the same example but this time we want to introduce a threshold for index change rather than making it hard code 0 as in all of our previous examples.

Before using remember(key) with threshold as a key, let’s take a code example without passing threshold as key .

@Composable
fun ScrollToTopBtnWithThreshold(lazyListState: LazyListState, threshold: Int) {
val isEnabled by remember { derivedStateOf { lazyListState.firstVisibleItemIndex > threshold } }
Button(onClick = { /*TODO*/ }, enabled = isEnabled) {
Text(text = "Scroll To Top")
}
}

In the code we are checking lazyListState.firstVisibleItemIndex against threshold being passed to composable from outside, imagine we start with threshold value 0 and user scrolls lazyListState.firstVisibleItemIndex to 10 so Scroll To Top Button will be enabled, but after some time the threshold value changes to 20 the Scroll To Top button will still be enabled.

What is happening here? For the first time remember lambda executes and a derived StateObject is created and remembered with the threshold value 0 but when threshold value changes to 20 remember lambda is not executed again with new threshold value because remember was not observing for change in threshold so there was no effect on the UI.

In order to make code work with threshold change, we have to pass threshold as key to remember in order to react to change in threshold .

Let’s see the code below with this change.

@Composable
fun ScrollToTopBtnWithThresholdAsKey(lazyListState: LazyListState, threshold: Int) {
val isEnabled by remember(threshold) { derivedStateOf { lazyListState.firstVisibleItemIndex > threshold } }
Button(onClick = { /*TODO*/ }, enabled = isEnabled) {
Text(text = "Scroll To Top")
}
}

In the code above when threshold value changes the remembered lambda is executed again and new StateObject is created with new threshold value and cached at the place of old one.

Generally change in threshold is to happen less frequently than index changes .

derivedStateOf uses input values to calculate an output value, when more than one input values are used to calculate derived value and the one of the values changes less frequently than other input value then that less frequently changed value should be added as key to remember block

When should we not use derivedStateOf? and prefer remember(keys) over derivedStateOf?

When the input value changes as much as the output value then we should not use derivedStateOf . e.g a use-case can be to show full name combining firstName and lastName. In this case output will change as much as the input changes so we can use remember passing in firstName and lastName as keys and lambda will provide output as fullName as in the code below.

val fullName = remember(firstName, lastName) {
    "$firstName $lastName"
}

As soon as one of the parameter value changes it will calculate a new full name and remember it.

Applications of derivedStateOf.

There are many scenarios where derivedStateOf must be used. I will show some of them where I have used it or have seen it somewhere.

  • I used it in a ViewPager example where showing next and prev buttons on ViewPager checking which page indexis visible in order to show/hide these buttons. I have written a detailed story about it you can read from there.
  • To implement collapsing/expanding behaviour on TopAppBar , checking collapsed fraction on a scrolling behaviour state, you can read from there following link below
  • Showing Scroll To Top button when the list scrolls to a certain index. We have used this example in this story and I will attach the Github link down below.
Sources
Github

Hope it was helpful 🙂

Remember to follow and 👏 if you liked it 🙂

— — — — — — — — — — —

GitHub | LinkedIn | Twitter

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu