A Guide to State Management Functions in Jetpack Compose

Nada Feteiha
9 min readMar 9, 2023

--

In this article, we will delve into the topic of state management in Jetpack Compose — a contemporary toolkit for developing native Android UIs. Our focus will be on exploring the key functions for state management such as remember, rememberSaveable, and derivedStateOf. These functions play a critical role in managing an app’s user interface and we aim to provide Android developers with a comprehensive understanding of how to use them effectively to build UIs that are both robust and performant.

Remember

remember is a Jetpack Compose function that is used to store and manage data. it is used to manage data within the lifecycle of a Composable function.

remember is similar to var or val in Kotlin, but with an important difference: the value of a remember variable is retained across recompositions of a Composable function. This means that the value of the variable is preserved even if the Composable function is re-executed.

Here’s an example of how to use remember to manage the state of a checkbox:

var isChecked by remember { mutableStateOf(false) }
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it }
)

In the above example, the isChecked variable is initialized with a default value of false. When the user checks or unchecks the Checkbox, the onCheckedChange function updates the value of isChecked. Because isChecked is declared using remember, the value is preserved across recompositions of the Composable function.

RememberSaveable

rememberSaveable is a Jetpack Compose function that allows you to save and restore data between app launches. This is especially useful for data that needs to persist across device restarts, such as user preferences or app settings.

The rememberSaveable function takes two arguments: the key that will be used to store the data in the SavedStateHandle, and the default value of the data. The SavedStateHandle is a container that stores key-value pairs of data, which can be saved and restored across app restarts.

Here’s an example of how to use rememberSaveable to save and restore a user's name:

var name by rememberSaveable { mutableStateOf("") }
TextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)

In this example, the name variable is initialized with an empty string as the default value. When the user enters their name in the TextField, the onValueChange function updates the value of name. The rememberSaveable function ensures that the value of name is saved in the SavedStateHandle and restored when the app is restarted.

rememberSaveable is a powerful tool for state management in Jetpack Compose that allows us to save and restore state across configuration changes and process death. However, there are some limitations to using rememberSaveable that are important to keep in mind:

  • Limited storage capacity: rememberSaveable relies on the Android framework's SavedStateHandle to store and retrieve data. The SavedStateHandle has a limited storage capacity, which means that we should be careful about what data we store using rememberSaveable.
  • Not suitable for large amounts of data: As rememberSaveable uses SavedStateHandle to store and retrieve data, it is not suitable for large amounts of data. We should avoid storing large objects or data sets using rememberSaveable to prevent performance issues and slow UI rendering.
  • Not always reliable: While rememberSaveable is designed to handle configuration changes and process death, it is not always reliable. There may be cases where the saved state is lost, and the app may need to be restarted. Therefore, we should not rely solely on rememberSaveable for critical data that cannot be lost.
  • Only supports basic data types: rememberSaveable only supports basic data types such as Boolean, Int, Float, String, and Parcelable objects. Complex data structures such as List or Map are not supported by rememberSaveable.

DerivedStateOf

derivedStateOf is a function provided by Jetpack Compose that allows you to compute a value based on one or more other pieces of state, and caches the result. It is similar to remember, but instead of holding a piece of state directly, it computes a derived value based on other state objects.

@Composable
fun Calculator() {
val num1 = remember { mutableStateOf(0) }
val num2 = remember { mutableStateOf(0) }
val operator = remember { mutableStateOf("+") }

val result = derivedStateOf {
when (operator.value) {
"+" -> num1.value + num2.value
"-" -> num1.value - num2.value
"*" -> num1.value * num2.value
"/" -> num1.value / num2.value
else -> 0
}
}

Column {
TextField(
value = num1.value.toString(),
onValueChange = { value ->
num1.value = value.toIntOrNull() ?: 0
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
TextField(
value = num2.value.toString(),
onValueChange = { value ->
num2.value = value.toIntOrNull() ?: 0
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
DropdownMenu(
expanded = false,
onDismissRequest = { },
modifier = Modifier.fillMaxWidth()
) {
DropdownMenuItem(onClick = { operator.value = "+" }) {
Text(text = "+")
}
DropdownMenuItem(onClick = { operator.value = "-" }) {
Text(text = "-")
}
DropdownMenuItem(onClick = { operator.value = "*" }) {
Text(text = "*")
}
DropdownMenuItem(onClick = { operator.value = "/" }) {
Text(text = "/")
}
}
Text(text = "Result: $result")
}
}

In this example, we’re creating a simple calculator that allows the user to enter two numbers and select an operator (+, -, *, or /) to perform a calculation. We’re using mutableStateOf to manage the state of the two numbers and the selected operator, and we're using derivedStateOf to compute the result of the calculation based on the current values of the numbers and the operator.

Within the Calculator composable function, we're creating three pieces of state using mutableStateOf: num1, num2, and operator. We're then using derivedStateOf to compute the result of the calculation based on the current values of these three pieces of state. The computation logic is defined in a lambda expression passed to derivedStateOf, which returns the result based on the current values of num1, num2, and operator.

Finally, we’re displaying the calculator UI using various Jetpack Compose widgets like TextField, DropdownMenu, and Text. The result of the calculation is displayed using a Text widget that takes the result value as an argument. The result value is recomposed whenever the values of num1, num2, or operator change, thanks to the caching provided by derivedStateOf.

When to use each function?

Deciding which function to use in Jetpack Compose relies on the particular needs of your app’s user interface and how you intend to handle its state. Here are some general principles to consider when choosing between the different state management functions.

  • Use remember when you want to store a value that doesn't depend on any other values and needs to persist across recompositions. For example, you might use remember to store a color theme or a font size that the user has selected.
  • Use rememberSaveable when you want to store a value that should survive configuration changes, such as screen rotations. rememberSaveable is similar to remember, but it also automatically saves the value to the SavedStateHandle so that it can be restored after the configuration change.
  • Use derivedStateOf when you want to compute a value based on other values and have the value updated automatically whenever any of the dependent values change. For example, you might use derivedStateOf to compute the total price of items in a shopping cart based on the individual item prices.

In addition to remember, mutableStateOf, derivedStateOf, and rememberSaveable, there are also other libraries that can be used for state management in Jetpack Compose, such as kotlin-parcelize, MapSaver, and ListSaver.

kotlin-parcelize: This library provides a convenient way to serialize and deserialize Kotlin data classes for use in Android's Parcelable interface. It can be used to pass data between different components of an app, such as between activities or fragments. In Jetpack Compose, kotlin-parcelize can be used with rememberSaveable to save and restore complex objects

@Parcelize
data class User(val name: String, val age: Int): Parcelable

@Composable
fun UserProfile(user: User) {
val savedUser = remember { mutableStateOf(user) }
Column {
TextField(value = savedUser.value.name, onValueChange = { savedUser.value = savedUser.value.copy(name = it) })
TextField(value = savedUser.value.age.toString(), onValueChange = { savedUser.value = savedUser.value.copy(age = it.toInt()) })
Button(onClick = { /* update user profile */ }) {
Text(text = "Update")
}
}
}

In the above example, we’re defining a User data class that's annotated with @Parcelize, which generates a Parcelable implementation for the class. We're then using remember to create a mutableStateOf object that holds a User instance, which is passed as a parameter to the UserProfile composable function.

Within the UserProfile composable function, we're using TextField widgets to allow the user to edit the name and age properties of the User object. We're then using a Button widget to trigger an action that updates the user profile.

By using remember to manage the User object, we can ensure that the user's changes are retained across recompositions and other state updates in the app. And by using @Parcelize, we can easily serialize and deserialize the User object for use with rememberSaveable to save and restore the object when the app is paused or destroyed.

MapSaver: This library provides a simple way to save and restore Map objects in Android. It can be used with rememberSaveable to save and restore Map state in Jetpack Compose. MapSaver allows us to easily manage complex data structures and can be used to store key-value pairs that need to be retained across recompositions.

@Composable
fun FavoriteColors() {
val colors = remember { mutableStateOf(mapOf("Alice" to Color.Red, "Bob" to Color.Blue)) }
val mapSaver = rememberSaveable(saver = MapSaver<String, Color>("favorite_colors")) { colors.value }
Column {
colors.value.forEach { (name, color) ->
Row {
Text(text = "$name's favorite color is:")
Spacer(Modifier.width(8.dp))
Box(Modifier.size(32.dp).background(color))
}
}
Button(onClick = { colors.value = colors.value + ("Charlie" to Color.Green) }) {
Text(text = "Add Charlie")
}
}
}

In the above example, we’re using remember to create a mutableStateOf object that holds a Map of strings to Color values representing people's favorite colors. We're also using rememberSaveable to create a MapSaver that saves and restores the Map when the app is paused or destroyed.

Within the FavoriteColors composable function, we're using a Column to display the list of people and their favorite colors. We're then using a Button to add a new person ("Charlie") with a favorite color of green to the Map.

By using remember to manage the Map of favorite colors, we can ensure that the user's changes are retained across recompositions and other state updates in the app. And by using rememberSaveable with MapSaver, we can easily save and restore the Map when the app is paused or destroyed.y

You can use ListSaverto avoid needing to define the keys for the map

ListSaver: This library provides a simple way to save and restore List objects in Android. It can be used with rememberSaveable to save and restore List state in Jetpack Compose. ListSaver allows us to easily manage large sets of data and can be used to store data that needs to be retained across recompositions.

@Composable
fun TodoList() {
val todos = remember { mutableStateOf(listOf("Buy groceries", "Take out the trash")) }
val listSaver = rememberSaveable(saver = ListSaver("todo_list")) { todos.value }
Column {
todos.value.forEach { todo ->
Text(text = todo)
}
TextField(
value = "",
onValueChange = { newTodo ->
todos.value = todos.value + newTodo
},
placeholder = { Text(text = "Add new task...") }
)
}
}

In the above example, we’re using remember to create a mutableStateOf object that holds a List of strings representing a to-do list. We're also using rememberSaveable to create a ListSaver that saves and restores the List when the app is paused or destroyed.

Within the TodoList composable function, we're using a Column to display the list of to-do items. We're then using a TextField widget to allow the user to add new to-do items to the list.

By using remember to manage the List of to-do items, we can ensure that the user's changes are retained across recompositions and other state updates in the app. And by using rememberSaveable with ListSaver, we can easily save and restore the List when the app is paused or destroyed.

In summary, remember and mutableStateOf are used to manage pieces of state within a composable function, but they do not provide state restoration when the app is paused or destroyed. derivedStateOf is used to compute a value based on other state objects, but it also does not provide state restoration. rememberSaveable is used to create a SaveableStateHolder that can save and restore state when the app is paused or destroyed, but it requires a saver function to be provided to handle serialization of the state.

--

--