Everything You Need To Know About Remember In Android Jetpack Compose
In this blog, we are going to talk about the importance of using remember in our state.
@Composable
fun CounterScreen() {
Column(
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Counter: ",
)
Button(
onClick = {
}) {
Text(
text = "Increase",
)
}
}
}
As you can see here we have a Composable: CounterScreen, it has a Column in that Colum we Have a Text that says Counter: and we also have a Button when clicked it does nothing, and in that Botton, we have a Text that says Increase.
Now we will see why it is so important always to use remember when it comes to state.
Let's add this state:
var counter by mutableStateOf(0)
Don’t worry about the ‘ by ’ for now, we’ll explain it in our upcoming blog. But for now, let’s consider this as our state but first, if you add this state you will see that there is an error on the mutableState, so let's silence it using this annotation: @SuppressLint(“UnrememberedMutableState”)
let’s now display the counter in the text by calling it and increase its value when the button is clicked
Our code should look like this :
@SuppressLint("UnrememberedMutableState")
@Composable
fun CounterScreen() {
var counter by mutableStateOf(0)
Column(
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Counter: $counter",
)
Button(
onClick = {
counter++
}) {
Text(
text = "Increase",
)
}
}
}
Now if we run our application and click on the Button that says Increase nothing will happen our counter won’t change values, and that’s because the state counter is not remembered, what actually happens is that the counter does increase in value, so the state changes of the counter and whenever the state changes in a Composable that Composable (for our case its CounterScreen) will recompose (Recompose in Jetpack Compose means the phone automatically updates the screen to show changes in an app without the programmer doing it manually) and since it will recompose and our state is not remembered it will show the initial value of the counter which is 0, so the counter is always initialized with 0 whenever we hit the Increase Button, it will increase the counter from 0 to 1 but since the Composable is recomposed and our state is not remembered it will immediately be set to 0 again
Now if we add remember like so
var counter by remember { mutableStateOf(0) }
Our counter will increase and it will work perfectly fine.
If we go deeper into how the remember Composable fun works we can click on cmd + right click in mac or windows + right click to access the code of it, which look like so :
/**
* Remember the value produced by [calculation]. [calculation] will only be evaluated during the composition.
* Recomposition will always return the value produced by composition.
*/
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
As you can see it’s an inline function which can result in better performance and optimization (we will talk about inline functions in the upcoming blogs)
its also a generic type parameter <T> that allows the function to work with different primitive types such as String, Int, custom data classes and even complex types like Composeable functions (it does not work will all types though, we will talk about that in a later blog)
it takes in a lambda function that returns a generic type and takes nothing as a parameter (the crossinline is related to the inline fun which we will talk about in the upcoming blogs )
@DisallowComposableCalls
is an annotation used to indicate that the lambda calculation
should not contain calls to other composables, meaning if for example that notation was not there so we can call composables there like so :
var counter by remember {
Text(text = "abc")
}
this would lead to unexpected behavior and potentially cause infinite recomposition loops, so the @DisallowComposableCalls
is there to ensure that no composable will be invoked inside the calculation lambda
this remember function returns a :
currentComposer.cache(false, calculation)
which is responsible for caching the value produced by the calculation
lambda. Let's break down what happens in this line:
currentComposer
refers to the current Composer instance, which is an internal object used by the Compose framework for managing the composition and recomposition process.
val currentComposer: Composer
@ReadOnlyComposable
@Composable get() { throw NotImplementedError("Implemented as an intrinsic") }
val currentComposer: Composer
: Here, currentComposer
is declared as a read-only property of type Composer
. The Composer
type represents the composition state and is an internal component of the Jetpack Compose framework.
@ReadOnlyComposable
annotation: This annotation indicates that the property getter is annotated with @Composable
, but it should only be used for reading data during composition and not for modifying the composition.
get() { throw NotImplementedError("Implemented as an intrinsic") }
: This is the getter method (we will talk about getters and setters in the upcoming blogs ) for the currentComposer
property. However, it throws NotImplementedError
with a message indicating that it is implemented as an intrinsic:
- In this case, “implemented as an intrinsic” means that the actual implementation of
currentComposer
is provided by the Jetpack Compose framework itself, and it is not meant to be implemented or overridden by users of the framework. - The purpose of throwing a
NotImplementedError
is to indicate that the getter should not be used directly and that it is not meant to be called from user code.
The cache
extension function (more about extension functions in the upcoming blogs ) is called on the currentComposer
instance. This function is an intrinsic API (as we saw earlier intrinsic means that it’s built-in or inherent to the language or framework itself. It implies that the functionality is provided directly by the language or framework, rather than being implemented externally or through user-defined code) provided by the Compose compiler plugin and is not meant to be called directly in user code.
@ComposeCompilerApi
inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T {
@Suppress("UNCHECKED_CAST")
return rememberedValue().let {
if (invalid || it === Composer.Empty) {
val value = block()
updateRememberedValue(value)
value
} else it
} as T
}
here the @ComposeCompilerApi
just means that this function is intended for internal use by the Compose framework and should not be called directly by user code.
we use inline fun for better performance, a generic type parameter T that allows the function to work with different types of values.
the Composer.cache means that it is an extension function more on that in the upcoming blog
this function takes 2 parameters a Boolean parameter indicating whether the cached value is invalid and needs to be recomputed, because if it the value doesn't changes or your state doesn't changes there is no need for a recomposition, and we have a block
which is a lambda expression that takes no arguments and returns a value of type T
. The @DisallowComposableCalls
annotation on the lambda indicates that it should not contain calls to other composables , also it should be this way just because the parameter calculation in the remember fun is the same type and its passed in the block lambda.
@Suppress("UNCHECKED_CAST")
: This is a suppression annotation used to suppress the warning about an unchecked cast. In this case, it indicates that the cast to type T
in the return statement is intentionally unchecked.
rememberedValue()
: This is a function call that retrieves the previously remembered value for the composition. It returns the value that was stored during the previous composition.
let { }
scoping function in Kotlin is often used for null safety and concise null-checking operations (more on let in the and null-checking in the upcoming blogs)
if (invalid || it === Composer.Empty) { ... } else it
: This is a conditional statement that checks if the cached value is invalid or if it is empty. If either condition is true, it means that a new value needs to be computed. In that case, the block()
lambda is called to compute the new value. The updateRememberedValue(value)
function is then used to update the cached value with the newly computed value. Finally, the computed value is returned. If the cached value is valid and not empty, it is simply returned as is.
and finally The as T
in the code snippet is a type cast operation (more on that in the upcoming blogs) . It is used to explicitly cast the value returned from the let { }
block to type T
.
and yah that’s pretty much it how the remember function works also when you click cmd + right click or win + right click on the remember fun you will realize that there is another remember function that they differ with the parameter key, it could be one key, two keys, three keys or even a number of arguments of keys :
@Composable
inline fun <T> remember(
key1: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(currentComposer.changed(key1), calculation)
}
/**
* Remember the value returned by [calculation] if [key1] and [key2] are equal to the previous
* composition, otherwise produce and remember a new value by calling [calculation].
*/
@Composable
inline fun <T> remember(
key1: Any?,
key2: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(
currentComposer.changed(key1) or currentComposer.changed(key2),
calculation
)
}
/**
* Remember the value returned by [calculation] if [key1], [key2] and [key3] are equal to the
* previous composition, otherwise produce and remember a new value by calling [calculation].
*/
@Composable
inline fun <T> remember(
key1: Any?,
key2: Any?,
key3: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(
currentComposer.changed(key1) or
currentComposer.changed(key2) or
currentComposer.changed(key3),
calculation
)
}
/**
* Remember the value returned by [calculation] if all values of [keys] are equal to the previous
* composition, otherwise produce and remember a new value by calling [calculation].
*/
@Composable
inline fun <T> remember(
vararg keys: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T {
var invalid = false
for (key in keys) invalid = invalid or currentComposer.changed(key)
return currentComposer.cache(invalid, calculation)
}
the key
parameter is very important and it is used to determine whether the composition needs to recompose and recompute the value stored in memory.
here is a quick example of that :
@SuppressLint("UnrememberedMutableState")
@Composable
fun CounterScreen() {
var counter by remember {
mutableStateOf(0)
}
var counter2 by remember {
mutableStateOf(counter)
}
Column(
modifier = Modifier.fillMaxSize().padding(12.dp)
) {
Text(
text = "Counter: $counter",
)
Button(
onClick = {
counter++
}) {
Text(
text = "Increase",
)
}
Text(
text = "Counter: $counter2",
)
Button(
onClick = {
counter2++
}) {
Text(
text = "Increase",
)
}
}
}
to the initial code, I just added another counter2 with its own text to display and its own button to click, this counter2 takes the first counter as its initial value or initial state
now since the counter2 is related to the first counter, when clicking on the button related to the first counter, the counter2 should be updated and have the same value as our first counter, right?
not really, what will happen actually is that even though the counter2 have the counter as it’s initial state when clicking on the button related to the first counter it will only update the state of the first counter and the state of the counter2 will be stuck at it’s initial state, this happens because it’s true that we assigned that we assigned to the counter2 the value of counter, that doesn't really mean that whenever the counter changes the counter2 will takes it value
var counter2 by remember {mutableStateOf(counter)}
this basically means that we just assigned to the counter2 the value counter that’s it, nothing will happen if the counter value is changed unless we use the key parameter like so :
var counter2 by remember(key1 = counter) { mutableStateOf(counter) }
this basically means that we have a key that is counter when counter
changes, the composition will recognize that the key for counter2
has changed, triggering the recomposition and updating the counter2
value accordingly.
Hopefully, this was helpful to you or at least was a reminder for you about the remember composable function.