Managing State in Jetpack Compose
Learn the differences between stateless and stateful composables and how state hoisting can help make your composables more reusable. By Rodrigo Guerrero.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Managing State in Jetpack Compose
30 mins
- Getting Started
- Introducing Jetpack Compose
- Understanding Unidirectional Data Flow
- Learning About Recomposition
- Creating Stateful Composables
- Creating Composable for Text Fields
- Creating Radio Button Composable
- Creating Composable for DropDown Menu
- Creating Stateless Composables
- State Holders
- Implementing State Hoisting
- Implementing State Hoisting in Other Composables
- Implementing the Register and Clear Buttons
- Fixing the Users List
- Where to Go From Here?
Implementing State Hoisting
Open RegisterUserComposables.kt. Start implementing state hoisting on EditTextField(). This composable needs two variables for its state: one that holds the text that the user is typing and another that holds the state to show whether there is an error. Also, it needs a callback to notify the state holder that the text value changed.
Add the following lines at the top of the parameters list in EditTextField():
// 1.
value: String,
// 2.
isError: Boolean,
// 3.
onValueChanged: (String) -> Unit,
Here is an explanation for this code:
-
valuewill receive the current text value for theEditTextField. -
isErrorindicates whether the current text value is valid or invalid so theEditTextFielddisplays an error indicator, if needed. -
onValueChangedwill execute whenever thevaluechanges.
Next, remove the following code:
val text = remember {
mutableStateOf("")
}
Because you are now receiving the state in these parameters, the composable doesn’t need to remember its state.
Now, update OutlinedTextField() as follows:
OutlinedTextField(
// 1.
value = value,
// 2.
isError = isError,
// 3.
onValueChange = { onValueChanged(it) },
leadingIcon = { Icon(leadingIcon, contentDescription = "") },
modifier = modifier.fillMaxWidth(),
placeholder = { Text(stringResource(placeholder)) }
)
In this code, you:
- Use
valueto set the current value of theOutlinedTextField(). - Set the value of
isErrorusing the parameter. - Execute
onValueChanged()when thetextparameter changes. Now you don’t need to update theremember()value — you only need to hoist this value up.
Amazing! EditTextField is now stateless. Because you are implementing state hoisting, now RegistrationFormScreen() needs to receive the state for the EditTextFields.
Add the following parameters to RegistrationFormScreen():
// 1.
registrationFormData: RegistrationFormData,
// 2.
onEmailChanged: (String) -> Unit,
// 3.
onUsernameChanged: (String) -> Unit,
With this code, you added:
- A
registrationFormDatavalue that contains all the data needed for the registration form. -
onEmailChanged()that will execute when the user updates the email text field. -
onUsernameChanged()that will execute when the user updates the username text field.
Finally, you need to pass these values to each EditTextField. Update both EditTextFields as follows:
EditTextField(
leadingIcon = Icons.Default.Email,
placeholder = R.string.email,
// 1.
value = registrationFormData.email,
// 2.
isError = !registrationFormData.isValidEmail,
// 3.
onValueChanged = { onEmailChanged(it) }
)
EditTextField(
leadingIcon = Icons.Default.AccountBox,
placeholder = R.string.username,
modifier = Modifier.padding(top = 16.dp),
// 4.
value = registrationFormData.username,
// 5.
isError = false,
// 6.
onValueChanged = { onUsernameChanged(it) }
)
With this code, you:
- Use
registrationFormData.emailto set the email value. - Use
registrationFormData.isValidEmailto show whether there is an error in the email field. - Execute
onEmailChanged()whenever the email value changes. - Use
registrationFormData.usernameto set the username value. - Set
isErrortofalsesince this field doesn’t have validation. - Execute
onUsernameChanged()whenever the username value changes.
Open MainActivity.kt and add the following line below the formViewModel declaration:
val registrationFormData by formViewModel.formData.observeAsState(RegistrationFormData())
import androidx.compose.runtime.getValue at the top of the MainActivity.
formViewModel contains a LiveData variable called formData that contains the state for the registration screen.
In this line, you observe this variable as state, using observeAsState(). You need to set its default value. You can use the default values with RegistrationFormData(), which make the form have empty text fields and a preselected radio button.
Whenever a value in formData changes, FormViewModel has logic to update registrationFormData‘s state. This new value propagates down to the composables that use it, triggering the recomposition process.
Finally, update the call to RegistrationFormScreen() like this:
RegistrationFormScreen(
// 1.
registrationFormData = registrationFormData,
// 2.
onUsernameChanged = formViewModel::onUsernameChanged,
// 3.
onEmailChanged = formViewModel::onEmailChanged,
)
In this code, you:
- Pass the
registrationFormDatastate to the registration form screen. - Call
onUsernameChanged()within theViewModelwhen the username changes. This function updates theregistrationFormDatacontent with the new username value. - Call
onEmailChanged()within theViewModelwhen the email changes. This function updates theregistrationFormDatacontent with the new email value.
Build and run. Open the registration screen. You can now add an email and username and see the value you type on the screen. Also, you can now check whether the email you entered is valid. Hooray!

Next, you’ll implement state hoisting to make the radio buttons and drop-down work.
Implementing State Hoisting in Other Composables
It’s time to make the radio button and drop-down composables stateless. Open RegisterUserComposables.kt and add the following parameters to RadioButtonWithText, above the text parameter:
isSelected: Boolean,
onClick: () -> Unit,
Here you pass the radio button isSelected parameter along with its onClick() callback.
Now, remove the following code:
val isSelected = remember {
mutableStateOf(false)
}
Because RadioButtonWithText now receives its state, this remember() is no longer needed.
Next, update the RadioButton composable like this:
RadioButton(
selected = isSelected,
onClick = { onClick() }
)
Here you assign both the value and the callback. With this, you made RadioButtonWithText() stateless.
The drop-down menu works differently than the previous composables. In this case, the drop-down menu needs a state that indicates whether it’s in its expanded state. You don’t need to hoist this state because it’s only needed by the drop-down composable itself.
On the other hand, this component needs state that has the selected item. In this case, you need a hybrid composable: part of its state is hoisted while the component still has some intrinsic state.
Update DropDown() adding these parameters above the menuItems parameter:
selectedItem: String,
onItemSelected: (String) -> Unit,
selectedItem will hold the selected value and onItemSelected() is the callback that executes when the user selects an item.
Next, remove the following code:
val selectedItem = remember {
mutableStateOf("Select your favorite Avenger:")
}
Because the composable receives selectedItem, it doesn’t need this remember() anymore.
Next, update Text(selectedItem.value), like this:
Text(selectedItem)
With this code, Text() uses the selectedItem parameter to display its value.
Finally, update DropDownMenuItem() as follows:
DropdownMenuItem(onClick = {
onItemSelected(menuItems[index])
isExpanded.value = false
}) {
Text(text = name)
}
Here, you call onItemSelected() when the user selects a value. With this code, DropDown() is now a hybrid composable.
Now, update the composables caller. Add the following parameters to RegistrationFormScreen(), below the line onUsernameChanged: (String) -> Unit,:
onStarWarsSelectedChanged: (Boolean) -> Unit,
onFavoriteAvengerChanged: (String) -> Unit,
Here, you updated the parameters to receive the different callbacks the radio buttons and drop-down menu need.
Update both radio buttons code within RegistrationFormScreen() like this:
RadioButtonWithText(
text = R.string.star_wars,
isSelected = registrationFormData.isStarWarsSelected,
onClick = { onStarWarsSelectedChanged(true) }
)
RadioButtonWithText(
text = R.string.star_trek,
isSelected = !registrationFormData.isStarWarsSelected,
onClick = { onStarWarsSelectedChanged(false) }
)
In the code above, you use the state parameters and assign the callbacks to both radio buttons.
Now, update the drop-down like this:
DropDown(
menuItems = avengersList,
// 1.
onItemSelected = { onFavoriteAvengerChanged(it) },
// 2.
selectedItem = registrationFormData.favoriteAvenger
)
Here, you:
- Execute the
onFavoriteAvengerChanged()callback when selecting an item. - Set the selected value to display your favorite Avenger.
Open MainActivity.kt and update RegistrationFormScreen() call like this:
RegistrationFormScreen(
registrationFormData = registrationFormData,
onUsernameChanged = formViewModel::onUsernameChanged,
onEmailChanged = formViewModel::onEmailChanged,
onStarWarsSelectedChanged = formViewModel::onStarWarsSelectedChanged,
onFavoriteAvengerChanged = formViewModel::onFavoriteAvengerChanged,
)
In this code, you pass the radio buttons and drop-down state variables and assign the event callbacks to the registration screen.
Build and run. Open the registration screen. Now, you can make a selection with the radio buttons and select your favorite Avenger, like in this image:

Finally, to finish this up, you’ll make the buttons work.