Lifecycle of Composables in Jetpack Compose
Learn about the lifecycle of a composable function and also find out how to use recomposition to build reactive composables. By Lena Stepanova.
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
Lifecycle of Composables in Jetpack Compose
25 mins
- Getting Started
- Exploring the Project Structure
- Identifying the Problem
- Lifecycle of a Composable
- Triggering Recomposition
- Defining the Source of Recomposition
- Observing Logs and Conditions
- Skipping Recomposition
- Smart Recomposition
- Interacting With ViewModels
- Passing Data From a ViewModel to a Composable
- Reacting With LiveData and SharedFlow
- Challenge: Adding LiveData for Questions
- Observing Composable Lifecycle
- Where to Go From Here?
Reactive programming is the backbone of Jetpack Compose. It allows you to build UI in a declarative manner. You no longer have to use getters and setters to change the views in response to underlying data. Instead, these changes happen reactively due to recomposition.
In this tutorial, you’ll learn:
- About the lifecycle of a composable function.
- Updating a composable from another composable.
- Observing changes in Logcat.
- How to use recomposition to build reactive composables.
Getting Started
Download the starter project by clicking Download Materials at the top or bottom of the tutorial. Unzip it and import the starter project into Android Studio.
Exploring the Project Structure
Unlike in a traditional Android project, in the QuizMe app you won’t find a layout resource directory here. The main directory is different as well – The ui directory has a screens package, which contains MainScreen, QuizScreen and ResultScreen. In these files, you’ll declare your UI. And you’ll do it all in Kotlin, with no more XML.
In the same directory, you’ll also find a theme package, which holds everything responsible for your UI items’ appearance.
Navigate to MainActivity.kt. Notice MainActivity
doesn’t extend AppCompatActivity
as usual but a ComponentActivity
. This is possible thanks to changes in the app build.gradle file. When you create a new project, you can start with an Empty Compose Activity, and Android Studio will add necessary dependencies for you.
But if you’re adding Jetpack Compose to an existing project, you first need to configure it in your app-level build.gradle file to comply with Jetpack Compose and then add the relevant dependencies.
Identifying the Problem
Now that you’re more familiar with the project, build and run it. You’ll see a quiz form.
Are you ready for a challenge? Try answering some of the questions.
Unfortunately, nothing appears if you start typing in the input fields. This is because your UI isn’t responsive yet. :[
Open QuizScreen.kt and take a look at QuizInput()
:
@Composable
fun QuizInput(question: String) {
Log.d(MAIN, "QuizInput $question")
val input = ""
TextField(
value = input,
onValueChange = { },
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
placeholder = {
Text(text = question)
}
)
}
This composable function is responsible for updating the text field for answers. It holds an input
variable, which is just an immutable String
. Currently, there’s no state that TextField()
could react to in order to update its value. The onValueChange
callback is empty as well.
It’s time to change this so your answers appear in the text field. But first, you need to learn about the lifecycle of a composable.
Lifecycle of a Composable
As part of the Android ecosystem, every composable has its own lifecycle. Luckily, it’s more simple compared to other components. Every composable goes through three stages of life:
- Entering the composition
- Recomposition
- Leaving the composition
A composable can recompose as many times as needed to render the latest UI.
Triggering Recomposition
In the starter project, you declare the composition within setContent()
in MainActivity
.
After you start QuizMe, your composables enter the composition declared in MainScreen()
which then calls QuizScreen()
with all its composables. But nothing gets recomposed afterward. QuizInput()
never enters the next stage of lifecycle because your UI remains static.
You need to give instructions to your composable functions to recompose and display the updated UI.
State plays an important role in declarative UIs. Recomposition happens when any of the states involved in the initial composition changes. Go ahead and change the immutable String
variable to a MutableState
variable, making the QuizInput()
stateful.
First, locate the input
variable and, instead of an empty text, use remember()
to create a mutable state:
var input by remember { mutableStateOf("") }
Now the input
is a state variable that holds a String
value. You use this value in TextField()
. Here, remember()
helps the composable keep the latest state of the input between recompositions.
Next, three lines below, update the onValueChange
callback:
onValueChange = { value -> input = value }
When the value of the text field changes, onValueChange()
gets triggered. It sets the value of the mutable state to the new input string and, as a result, Jetpack Compose reacts to this state change and recomposes TextField()
.
Build and run the app again. Try entering your answers to the questions now. When you enter a new letter in the text field, the recomposition happens, and you can see the latest input.
Well done!
But what if you want to trigger the recomposition of another composable in response to changes in QuizInput()
? For example, adding another TextField()
to your initial screen layout when the user selects the checkbox. Find the answer in the next paragraph.
Defining the Source of Recomposition
If you want to trigger the recomposition of one composable to another, use state hoisting and move the responsibility to the composable in control.
For example, look at CheckBox()
in QuizScreen.kt:
@Composable
fun CheckBox(
// 1
isChecked: Boolean,
onChange: (Boolean) -> Unit
) {
. . .
Checkbox(
// 2
checked = isChecked,
onCheckedChange = onChange
)
. . .
}
Here’s how it works:
- It receives a
Boolean
checked state and anonChange
lambda as parameters. This is useful because you can reuse the composable multiple times and there won’t be any side-effects. Also, you added flexibility to the composable function because it can receive the state from anywhere now. - You assigned the value and lambda to the
Checkbox
component. Now, if the user changes thechecked
state, the component reacts with theonChange
lambda, lifting the current state ofisChecked
toQuizScreen()
.
Look at the following lines in QuizScreen()
:
var checked by remember { mutableStateOf(false) }
val onCheckedChange: (Boolean) -> Unit = { value -> checked = value }
These mean QuizScreen()
is the actual holder of the checked
state value. It’s in control of the value and can change that value when it gets an onCheckedChange
callback.
As a result, you can use the checked
state from QuizScreen()
to trigger recomposition of other composables.
Next, you want to disable submitting the form if the box isn’t checked at least once. Change the signature of SubmitButton()
like this:
@Composable
fun SubmitButton(isChecked: Boolean, text: String, onClick: () -> Unit)
Here you provide the state value to a composable function for the submit button.
Then, add a parameter to ExtendedFloatingActionButton()
inside SubmitButton()
:
backgroundColor = if (isChecked) MaterialTheme.colors.secondary else Color.Gray
With this line, you handle changing the background color of the submit button depending on the checked value.
Also, in QuizScreen()
, pass the checked
state to SubmitButton()
:
SubmitButton(checked, stringResource(id = R.string.try_me)) { }
Build and run the app. Try using the checkbox.
The button changes its color when the user selects or unselects the checkbox. Wow! You’re triggering the recomposition of the button in response to checkbox state changes.