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?
Observing Logs and Conditions
Open Logcat, select com.yourcompany.android.quizme and observe the recomposition in the log output:
D/MainLog: Checked state true
D/MainLog: Button recomposed
D/MainLog: Checked state false
D/MainLog: Button recomposed
As you can see, the recomposition happened as many times as you clicked on the checkbox.
Now add logic for disabling the button if the checkbox isn’t checked. In QuizScreen()
, add a condition for SubmitButton()
:
if (checked) {
SubmitButton(checked, stringResource(id = R.string.try_me)) { }
}
Now Compose won’t do the button recomposition if the box isn’t checked.
Build and run the app. Tap the checkbox a few times. Notice the button is visible only when you select the checkbox.
The log output is different as well:
D/MainLog: Checked state true
D/MainLog: Button recomposed
D/MainLog: Checked state false
D/MainLog: Checked state true
D/MainLog: Button recomposed
D/MainLog: Checked state false
Recomposition follows conditional rules, just like every other piece of code in Kotlin. When the checked state is set to false, there’s no need for Jetpack Compose to recompose the SubmitButton()
.
Skipping Recomposition
Jetpack Compose is intelligent and can choose to skip recomposition when it’s not needed.
Inside the if
clause, above SubmitButton()
, assign a new value to the questions
variable:
questions = listOf(
"What's the best programming language?",
"What's the best OS?",
"The answer to Life, The Universe and Everything?"
)
Here you added one more question to the list. This will result in adding a new input field dynamically the first time the user selects the checkbox.
Build and run the app. Select the checkbox and you’ll see a new text field appears:
Check the log output. You’ll see the following:
D/MainLog: Checked state true
D/MainLog: Button recomposed
D/MainLog: QuizInput The answer to Life, The Universe and Everything?
Notice that the logs contain only the new QuizInput
field name here. Jetpack Compose recognized that the first two items in the list hadn’t changed, so it didn’t have to recompose them.
When you select the checkbox, it sends an event to the QuizScreen()
, which sets the checked
state to the new value. In its turn, Jetpack Compose knows which composables to recompose. In the log output, you see that the CheckBox()
gets recomposed each time the user selects or unselects it. But the button gets recomposed only when the checked
state is true
. InputFields()
isn’t affected by the user interaction with the checkbox at all.
This happens because Jetpack Compose skips recomposition where possible to stay optimized. Another example of this is when a conditional statement defines when not to show a composable as with SubmitButton()
or when the state doesn’t affect a composable or the state hasn’t changed, as with InputFields()
.
Smart Recomposition
How does Jetpack Compose know whether the state has changed? It simply uses equals()
to compare the new and the old values of the mutable state. Therefore, any immutable type won’t trigger recomposition.
Do you remember trying to use InputFields()
with the immutable String
value at the beginning of the tutorial? Jetpack Compose didn’t want to recompose anything because the value couldn’t change.
Any immutable type is stable and doesn’t trigger recomposition. That’s why you use remember()
combined with mutableStateOf()
. Jetpack Compose observes changes of the mutable state so it knows when to start recomposition and which composable functions it should recompose.
Modify the previous code block like this:
questions = listOf(
"The answer to Life, The Universe and Everything?",
"What's the best programming language?",
"What's the best OS?"
)
Here you shuffled questions in the list.
Build and run the app again. Select the checkbox.
At first sight, it still looks the same. But in the log output, you’ll see Compose recomposed all three inputs:
D/MainLog: Checked state true
D/MainLog: Button recomposed
D/MainLog: QuizInput The answer to Life, The Universe and Everything?
D/MainLog: QuizInput What's the best programming language?
D/MainLog: QuizInput What's the best OS?
Once Jetpack Compose started to compare the new list with the old, it noticed that the first item didn’t equal the first item of the old list. In that case, it couldn’t reuse the old QuizInputs()
and had to recompose them all. But how to be sure Jetpack Compose will use old composables if there are any?
Modify QuizInputFields()
by wrapping QuizInput()
into key()
:
key(question){
QuizInput(question = question)
}
To ensure Jetpack Compose reuses old composables, you use key()
. The question
value you provided is identifier for a certain instance of QuizInput
.
Build and run the app again. Select the checkbox. The UI remains the same!
Recheck the log output. This time, it looks like when you added the new question to the bottom of the list – only the new InputField()
was recomposed:
D/MainLog: Checked state true
D/MainLog: Button recomposed
D/MainLog: QuizInput The answer to Life, The Universe and Everything?
Although the list order changed, Jetpack Compose reused two initial question fields because it could identify them this time. This is an important concept to remember when you need to render longer lists or when the list order is essential.
Interacting With ViewModels
What you want to do now is to verify the user inputs and let them know if they’re on the right track. Check out the repository and business folders in your project.
To produce questions and validate the answers, you’ll mock the backend in QuestionsRepository. You’ll use QuizViewModel to connect the UI with your data source.
Passing Data From a ViewModel to a Composable
Add these two functions to QuizViewModel
to fetch questions:
fun fetchQuestions(): List<String> {
return repository.getQuestions()
}
fun fetchExtendedQuestions(): List<String> {
return repository.getExtendedQuestions()
}
These will call the functions from the repository to fetch a basic and an extended list of questions, which you introduced earlier.
In QuizScreen.kt, find QuizScreen()
and replace the hardcoded listOf()
questions with the function call from quizViewModel
:
var questions by remember {
mutableStateOf(quizViewModel.fetchQuestions())
}
Also, above SubmitButton()
, modify the extended list by replacing listOf()
with this:
questions = quizViewModel.fetchExtendedQuestions()
Build and run the app. You shouldn’t notice any changes because recomposition still works as before. Only the data source has changed.
Congrats! You just connected QuizViewModel
with your composable function.