Everything you need to know about State in Jetpack Compose with examples
Jetpack Compose is a reactive framework. Instead of you having to call setText()
or setColor()
to update your UI, Compose will automatically update your UI when the state of the UI is updated. The way that Compose is aware about such state UI changes is by using Jetpack Compose's State
object.
In this article you will learn everything you need to know about States. What they are, how to create them and use them. You will learn about stateful and stateless composables and their gotchas. There is also a bonus section in the end 🎁.
What is state in Jetpack Compose?
In context of Jetpack Compose, a state is a value that is related to updating your UI. Every time a state value changes, Jetpack Compose will automatically update the UI for you.
The value of the state can be of any type. Sometimes it can be as simple as a Boolean
or a String
. Other times the state can be a complex data class that contains values for the entire rendered screen.
In order for Compose to be aware of state changes, your state values need to be wrapped in a State
object. You can do that using the mutableStateOf()
function. This will return a MutableState<T>
object and Compose will keep track of changes and update the UI whenever you modify the value.
🚫 Never update your state's value outside of the State
object. Otherwise Compose will not be aware of the changes and will not be able to update your UI.
How to create a state object in Jetpack Compose?
You have already seen this before:
var enabled by remember { mutableStateOf(true) }
this cryptic line of code is what is used in composable functions to represent state. It might look really confusing at first, so let's break down what is happening here:
mutableStateOf(true)
will produce aMutableState<Boolean>
object which will hold the value of our state (which istrue
).remember {}
tells Compose that it will need to remember the value passed to it, so that it does not execute the lambda on every recomposition.by
is a Kotlin keyword for delegates. It hides the fact that themutableStateOf()
returns aMutableState
object and it allows us to useenabled
as if it was a boolean instead.
Here is what would happening if any of those magic words are missing:
What if I don't use mutableStateOf()
?
@Composable
fun MyComponent() {
var enabled by remember { true }
// ...
Text("Enabled is ${enabled}")
}
👎 This won't work. Even though in our code we are able to modify the enabled
value, our UI will not be aware of such a change. As a result, your UI will not be updated when you update the boolean.
What if I don't use remember {}
?
@Composable
fun MyComponent() {
var enabled by mutableStateOf(true)
// ...
Text("Enabled is ${enabled}")
}
👎 This won't work either. As soon as you change enabled
to false
, Compose will correctly refresh your UI when you update your state. It will also execute mutableStateOf()
again, recreating the state object and rerendering your UI with the enabled
value being true
.
Remember this though (pun intended). We do not control how often or how many times our code is executed within our composable functions.
Note that this is true only if you are creating your states within a composable function. If you are creating a state object within a ViewModel
you do not need to wrap your states in a remember {}
function, but your ViewModel
needs to be remembered somehow. This is handled automatically for you via the dedicated viewModel {}
or hiltViewModel()
functions.
What if I don't use by
?
@Composable
fun MyComponent() {
var enabled = remember { mutableStateOf(true) }
// ...
Text("Enabled is ${enabled.value}")
}
🙆 This will work. What changes here is that enabled
is going to be a MutableState<Boolean>
reference. You will not be able to treat enabled
as a boolean anymore. In order to update or modify your state, you would need to access its value using state.value
as shown in the snippet above.
I personally find this more verbose and like the delegate version, but this is just code. There are no right or wrong ways of doing things so pick your favorite way.
Stateful vs stateless composables
A composable function that maintains its state is called stateful. A composable function that does not keep its state is called stateless.
Each have a place in a Jetpack Compose codebase and here is why:
When you want your composables to be stateless
In most cases, you want your composables to remain stateless. Ideally the state of the entire screen is calculated in one place (usually in your ViewModel
) which passes the state down to all your composables. This makes development and testing simpler than having to jump to multiple composable functions in order to understand what is going on.
A stateless composable looks like this:
@Composable
fun MyCustomButton(label: String, onClick: () -> Unit) {
Button(onClick) {
Text(label)
}
}
The MyCustomButton
composable relies on its caller to pass the label and the click listener and does not keep a state object. That is why it is a stateless composable.
When you want your composables to be stateful
Your screen level composable (the one responsible for rendering the entire screen) is a good candidate for maintaing the state of the entire screen.
Usually such composables have a reference to a ViewModel
which calculates the state of the entire screen. Once a new state is emitted, it is the screen level's composable to forward the necessary values from the state down to the children composables.
A screen level composable holding a ViewModel
looks like this:
@Composable
fun HomeScreen() {
val homeViewModel = viewModel { HomeScreenViewModel() }
val state by homeViewModel.inputText
// TODO use state
}
Exception to the rule: Why you might want your TextInput
to be stateful
Even though we discussed how you want your independent componets to remain stateless, there is one specific case that you might want to consider using a stateful composable instead.
If there is a delay between typing text into a TextInput
and updating the composable, you might see the weird behavior demonstrated in the following video if the text input is fast enough. In this example we are typing the same text and having a 100ms delay between the typing and the emition of a new state:
A simple workaround is having a stateful version of a TextInput
maintaining the text it needs to display and notify the callers via some onTextChanged
listener like this:
@Composable
fun StatefulTextField(
text: String,
onTextChanged: (String) -> Unit,
) {
var state by remember { mutableStateOf(text) }
TextField(
value = state,
onValueChange = {
state = it
onTextChanged(it)
}
)
}
this ensures that the TextField
is updated as soon as new text is typed.
As long as the text in your composable gets updated without any delay from the moment the text was typed, it will be alright. This also includes pushing your text updates to your ViewModel
and then immediately updating your state.
More info about this in this Github Repo or Google's Effective state managment for TextField
How to keep state within a ViewModel
Keeping state within a ViewModel
is similar to how you would do it within a composable function.
Use mutableStateOf()
to create the state object which you will be updating from within the ViewModel
when needed. Then expose the state object so that you screen level composable can be refreshed when the state changes:
class HomeScreenViewModel : ViewModel {
var inputText by mutableStateOf("")
private set
fun onTextChanged(text: String) {
viewModelScope.launch {
inputText = text
}
}
}
Note that in this case you do not need (and cannot) to use the remember {}
function within the ViewModel
. You cannot use it because remember {}
is a composable function and composable functions can only be used from other composable functions. You do not need to use it, as we are using the viewModel {}
function in our caller composable function. This function is responsible for maintaining the same instance of the ViewModel
among composable recompositions.
How to use state hoisting to keep your composables stateless
State hoisting in plain english means to remove any persistent state from a composable function. Instead you pass the state via the parameters of the function.
Here is a stateful composable:
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}
}
Let's use state hoisting and remove that mutableStateOf()
part from the function:
@Composable
fun StatelessCounter(count: Int, onClick : ()->Unit){
Button(onClick = onClick) {
Text("Clicked $count times")
}
}
that's it. Instead of keeping the state within the Counter
composable, we are expecting the count
value to display from the function's parameters.
Additionally we are also expecting a listener to be passed as a parameter so that we can notify the caller composables when the Button
is clicked.
As a result, the StatelessCounter
can be used in multiple places of the app, without tieing the UI logic to the counting logic.
How to update composables by mutating its state
The more you use the built-in composables (such as Scaffolds, Bottom Sheets, Drawer etc) you will quickly realize that states are everywhere in Jetpack Compose. Most components expose a state object parameter, which allow other composable functions to modify the state and update the composable. This is the closest pattern to Android View's setValue(x)
or getValue()
.
A good example of this is are bottom sheets:
val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
ModalBottomSheetLayout(
sheetContent = {
BottomSheetContent()
},
sheetState = sheetState
) {
Button(
onClick = {
scope.launch {
sheetState.show()
}
}) {
Text("Show Sheet")
}
}
In the above example, the sheetState
is used in the ModalBottomSheetLayout
and then passed to the Button
's onClick listener to modify its state. This is a common pattern in Jetpack Compose. rememberModalBottomSheetState()
is a convenient function that wraps remember { mutableStateOf(ModalBottomSheetState) }
for you.
Note that for your own composable functions there is no need to create dedicated classes to represent their state. In most cases, providing a simple String
parameter for what text needs to be displayed in your composable is more than enough.
🎁 BONUS: How can I use Kotlin's Flow, rxJava or LiveData to represent state in Jetpack Compose?
You can still use LiveData, RxJava Observables, Kotlin Flows to represent state in Jetpack Compose. All you have to do is use the respective extension functions. They convert the respective reactive object into a Jetpack Compose state.
How to use Kotlin's Flow in Jetpack Compose
In your composable function use:
val flow = MutableStateFlow("")
// ...
val state by flow.collectAsState()
// for lifecycle aware version
val state by flow.collectAsStateWithLifecycle()
How to use LiveData in Jetpack Compose
In your app/build.gradle
include the following dependency:
dependencies {
implementation "androidx.compose.runtime:runtime-livedata:x.y.z"
}
and in your composable functions use:
val liveData = MutableLiveData<String>()
// ...
val state by liveData.observeAsState()
How to use rxJava2 or rxJava3 in Jetpack Compose
In your app/build.gradle
include the following dependency:
dependencies {
implementation "androidx.compose.runtime:runtime-rxjava2:x.y.z"
// or implementation "androidx.compose.runtime:runtime-rxjava3:x.y.z"
}
and in your composable functions use:
val observable = Observable.just("A", "B", "C")
val state by observable.subscribeAsState("initial")
In this article you learnt everything you need to know to master state in Jetpack Compose. You learnt about the importance of the State
object, how to create it and how to use it. You also learnt the difference between stateful and stateless composables and when to use one vs the other. You should now be aware of the behavior in InputText
's delay and how to use state in ViewModels
.
Related reading
Google's Effective state management for TextField in Compose on medium.com
Recommended videos