This article is about how to manage the state in Android Jetpack Compose, state hoisting (stateless composable), and stateful versus stateless composable.
What is State in Jetpack Compose?
The state is an object that is connected/subscribed to one or more widgets, contains data, and is eager to update the widgets from that data.
If there’s any change happens in data, it will update all its subscribed UI widgets. The values of the state are changed at runtime.
The @ composable function(s) recomposes itself with the new data when the state/object value is updated and doesn’t affect or update the whole UI.
In Jetpack Compose, the composable are subscribed to a state, and when the value of the state is updated then all the composable who are subscribed to it also update the value.
For example, three texts are subscribed to a state, and when the value of the state changes, the text in these three would also be updated.
With Jetpack compose, we can preserve the state of view in major two ways:
- Using remember{}, a composable function that can store a single object in memory, or by using rememberSaveable{}, a composable function to restore your UI state after an activity or process is recreated (for example it happens when the screen is rotated in the Android application). We can manage a simple state of view within a compose function itself.
- Using ViewModel, a state holder to separate business logic from UI component. ViewModel has a longer lifecycle than the composition as they are lifecycle-aware components so ViewModel can preserve its state across configuration changes.
remember Composable
The remember{} can be used to store both mutable and immutable objects.
A value computed by remember{} is stored in the Composition during initial composition, and the stored value is returned during recomposition.
In each recomposition, remember() returns the stored value so the composable can use it. Whenever the stored value has to change, you can update it and remember() will store it. The next time a recomposition occurs, remember() will provide the latest value.
Key Term:
- Composition: a description of the UI built by Jetpack Compose when it executes composables.
- Initial composition: creation of a Composition by running composables the first time.
- Recomposition: re-running composables to update the Composition when data changes.
Using mutableStateOf()
To create a state, we need to use mutableStateOf() function. In this, the state stores the value on execution, and if any composable is subscribed to it, the composable updates the value if there are any changes.
There are three ways to declare a MutableState object in a composable:
1 . val mutableState = remember { mutableStateOf (default) }
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name = remember { mutableStateOf("") }
if (name.value.isNotEmpty()) {
Text(
text = "Hello, $name.value!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name.value,
onValueChange = { name.value = it },
label = { Text("Name") }
)
}
}
Using Kotlin’s Delegated Property to access the State
2 . var value by remember { mutableStateOf(default) }
In Kotlin, there is a feature called Delegated Property, which simply eliminates the need to access the value property every time.
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
//using kotlin's delegate property
var name by remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
3. val (value, setValue) = remember { mutableStateOf(default) }
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
val (name, setName) = remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { setName(it) },
label = { Text("Name") }
)
}
}
Output:
Note: If your device orientation gets changed, the value will reset.
If you want to keep the data even if the activity recreated/orientation change happened, use “rememberSavable”.
rememberSaveable Composable
Use rememberSaveable{} to restore your UI state after an activity or process is recreated. RememberSaveable retains state across recompositions. In addition, rememberSaveable also retains state across activity and process recreation.
Example:
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by rememberSaveable { mutableStateOf("") }
println("HelloContent(): $name")
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
Output:
State Hoisting
State hoisting in Compose is a pattern of moving state to a composable’s caller to make a composable stateless.
The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:
- value: T: the current value to display
- onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
Each composable can have many value parameters and many event callbacks.
Example:
@Composable
fun HelloScreen() {
var name by rememberSaveable{ mutableStateOf("") }
HelloContent(name = name, onNameChange = { name = it })
}
@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
By hoisting the state out of HelloContent, it’s easier to reason about the composable, reuse it in different situations, and test. HelloContent is decoupled from how its state is stored. Decoupling means that if you modify or replace HelloScreen, you don’t have to change how HelloContent is implemented.
Stateful versus Stateless
A composable that uses remember to store an object creates an internal state, making the composable stateful.
HelloContent is an example of a stateful composable because it holds and modifies its name state internally. This can be useful in situations where a caller doesn’t need to control the state and can use it without having to manage the state themselves. However, composables with internal state tend to be less reusable and harder to test.
A stateless composable is a composable that doesn’t hold any state. An easy way to achieve stateless is by using state hoisting.
The stateful version is convenient for callers that don’t care about the state, and the stateless version is necessary for callers that need to control or hoist the state.