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:
-
value
will receive the current text value for theEditTextField
. -
isError
indicates whether the current text value is valid or invalid so theEditTextField
displays an error indicator, if needed. -
onValueChanged
will execute whenever thevalue
changes.
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
value
to set the current value of theOutlinedTextField()
. - Set the value of
isError
using the parameter. - Execute
onValueChanged()
when thetext
parameter 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 EditTextField
s.
Add the following parameters to RegistrationFormScreen()
:
// 1.
registrationFormData: RegistrationFormData,
// 2.
onEmailChanged: (String) -> Unit,
// 3.
onUsernameChanged: (String) -> Unit,
With this code, you added:
- A
registrationFormData
value 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 EditTextField
s 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.email
to set the email value. - Use
registrationFormData.isValidEmail
to show whether there is an error in the email field. - Execute
onEmailChanged()
whenever the email value changes. - Use
registrationFormData.username
to set the username value. - Set
isError
tofalse
since 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
registrationFormData
state to the registration form screen. - Call
onUsernameChanged()
within theViewModel
when the username changes. This function updates theregistrationFormData
content with the new username value. - Call
onEmailChanged()
within theViewModel
when the email changes. This function updates theregistrationFormData
content 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.