Leave a rating/review
Notes: 04. Add State to Composables
The student materials have been reviewed and are updated as of September 2022.
Demo
So far you’ve been getting to know the Jetpack Compose framework, and it’s been going great! :]
You’ve learned lots about different types of components and composable functions, but you haven’t really handled or introduced any state to the UI.
Jetpack Compose has a really unique way of handling state, so let’s see how to implement it!
Open the AddBookActivity if you haven’t already. Add the following property at the top of the class:
private val _genresState = MutableLiveData(emptyList<Genre>())
You’ll use this state to store the genres that you can show in the dropdown menu! Now change the onCreate() function to load the genres as the screen is created:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { AddBookContent() }
loadGenres()
}
private fun loadGenres() {
lifecycleScope.launch {
_genresState.value = repository.getGenres()
}
}
This is important, as you need to have the genres ready to display them in a dropdown! Notice how you’re using coroutines here, as the repository is powered by suspend functions.
Next, add the following code to the the AddBookFormContent component:
val genres = _genresState.value ?: emptyList()
val isGenresPickerOpen = remember { mutableStateOf(false) }
These two properties will help you keep track of the genres you’re showing and the picker open state. Now add the rest of the properties.
val bookNameState = remember { mutableStateOf("") }
val bookDescriptionState = remember { mutableStateOf("") }
val selectedGenreName =
genres.firstOrNull { it.id == _addBookState.value?.genreId }?.name ?: "None"
There’s quite a few properties here, so let’s go over them one by one. You fetch the genres from the LiveData value, and also get the currently selected genre name, to display in the dropdown.
The other three values are using something called the remember wrapper. Compose doesn’t remember state in its components like regular Views. If you want to store a piece of information in Compose, you have to tell it to remember the state.
Additionally, if you want the state to be changed, you need to use the mutableStateOf() function, which is similar to a LiveData object.
[Slide 1 - State in compose]
So what previously happened with your input fields is that you were typing in them, but you weren’t updating their state. Because of this, there was no input change within the field.
[Slide 2 - Compose Drawing]
And compose always draws only by the declarative definition you give it. So if you don’t give it a way to update its data, it’s not going to change.
[Slides 3 & 4 - Function of state]
So when you tie in the mutable state to compose, it knows how to update, as it’s always drawing the UI based on the state.
This is why it’s also said that in Compose the UI is a function of state.
[Slide 5 & 6 - Recomposition]
But what happens when you change the state? In compose, something called a Recomposition triggers.
A recomposition is the process of calling your functions with new state, and re-drawing the UI, based on the said state. Recomposition is really smart, because it skips all the composable functions and lambdas that weren’t impacted by the state change, meaning it can re-compose efficiently, without having to re-draw everything.
[Switch back to code]
Now that you understand recomposition a bit better, you can proceed to fill in the rest of the state handling.
Add the following properties to the Input fields:
OutlinedTextField(
value = bookNameState.value, // new
onValueChange = { newValue -> // new
bookNameState.value = newValue
_addBookState.value = _addBookState.value?.copy(name = newValue)
},
label = { Text(text = stringResource(id = R.string.book_title_hint)) })
OutlinedTextField(
value = bookDescriptionState.value, // new
onValueChange = { newValue -> // new
bookDescriptionState.value = newValue
_addBookState.value =_addBookState.value?.copy(description = newValue)
},
label = { Text(text = stringResource(id = R.string.book_description_hint)) })
Here you defined that the input fields will change their state according to the bookNameState.value, and the description value. Any time those properties change, you’ll get a new data emission, and the component will recompose. You also update the addBookState to store the values for when you decide to add the book to your library.
Finally, wrap up the UI by adding the following handles for the DropdownMenu and the TextButton which toggles it:
Row {
TextButton(
onClick = { isGenresPickerOpen.value = true }, // new
content = { Text(text = stringResource(id = R.string.genre_select)) })
DropdownMenu(
expanded = isGenresPickerOpen.value, // new
onDismissRequest = { isGenresPickerOpen.value = false }) { // new
for (genre in genres) { // new
DropdownMenuItem(onClick = {
_addBookState.value = _addBookState.value?.copy(genreId = genre.id)
isGenresPickerOpen.value = false
}) {
Text(text = genre.name)
}
}
}
}
For the most part, you’re handling the way the dropdown is opened or closed within TextButton(). By adding appropriate handles, you don’t have to worry about the dropdown staying open when you select an item, or when you dismiss the window.
And in the dropdownContent part, you’re using a for loop to call the DropdownMenuItem function for each genre.
You might think you’re not actually creating the items here, but just by calling the functions you’re adding them to the Compose tree, so everything will work out!
You also hide the dropdown when you select an item, and update the addBookState.
Now build & run the app, and check the UI!
[Build & run]
Once you start writing in the input fields, you’ll see that the UI updates accordingly, and it’s seamless, even though the inputs are being re-composed! You can also pick a genre!
But wait, you’re not showing the picked genre to the user. Let’s change that.
Change the Row to add a Text, like so:
Row(verticalAlignment = Alignment.CenterVertically) { // alignment
...
Text(text = selectedGenreName) // text element
}
By adding Text to the Row, a horizontal linear component, you can have the button for the select genre option, and a text to represent the currently selected genre.
Additionally, you added Alignment.CenterVertically, to keep both of the items aligned. Now build & run the app again, and everything should be fine, and you should be able to add books to your library!
[Build & Run, add book]
Awesome! You can see how easy to use and awesome Compose is!
In the next few episodes you’ll see how to make your components reusable, and clean up their design to be more material! See you there! :]