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?
Reacting With LiveData and SharedFlow
When communicating with your ViewModel from Jetpack Compose UI, you have several options for reacting to changes.
Navigate to QuizViewModel.kt and look at the following variables:
// 1
private val _state = MutableLiveData<ScreenState>(ScreenState.Quiz)
val state: LiveData<ScreenState> = _state
// 2
private val _event = MutableSharedFlow<Event>()
val event: SharedFlow<Event> = _event
Here’s the explanation:
-
stateholds the state of the screen and signals to Jetpack Compose when to switch fromQuizScreen()toResultScreen(). -
eventhelps you interact with the user by showing dialogs or loading indicators.
These variables hold a ScreenState or an Event instance, which are sealed classes at the bottom of QuizViewModel.
Go back to QuizScreen.kt. In QuizScreen(), add a map of answers above the onAnswerChanged value:
val answers = remember { mutableMapOf<String, String>() }
Using remember(), you ensure the previous answers aren’t lost when QuizScreen() recomposes.
Then, complete the onAnswerChanged lambda:
answers[question] = answer
You use the question and answer values to set the data in the answers map.
Next, in QuizInput(), add a new parameter to the signature:
fun QuizInput(question: String, onAnswerChanged: (String, String) -> Unit)
Then, invoke onAnswerChanged once value changes. Modify code in the onValueChange lambda like this:
run {
input = value
onAnswerChanged(question, input)
}
When the user enters a new letter, the value of the input state changes and the new answer gets saved in the answers map.
To fix errors you made with the previous code, look for QuizInputFields() and provide the onAnswerChanged callback to QuizInput():
QuizInput(question = question, onAnswerChanged = onAnswerChanged)
Finally, you need to verify the answers once the user submits the quiz. In QuizScreen(), complete the callback in SubmitButton() with this:
quizViewModel.verifyAnswers(answers)
Now that you’ve connected your UI with QuizViewModel and QuestionsRepository, all that’s left to do is declare how your UI should react to changes.
Go to QuizViewModel.kt and check out verifyAnswers():
fun verifyAnswers(answers: MutableMap<String, String>) {
viewModelScope.launch {
// 1
_event.emit(Event.Loading)
delay(1000)
// 2
val result = repository.verifyAnswers(answers)
when (result) {
// 3
is Event.Error -> _event.emit(result)
else -> _state.value = ScreenState.Success
}
}
}
This function has several important points:
- When
verifyAnswers()is called, the SharedFlow emits aLoadingevent. - After 1 second,
verifyAnswers()from repository verifies the answers. - If the answers aren’t correct,
eventemits anError. Otherwise, you use LiveData to setstatetoScreenState.Success.
As you can see, event and state should be transmitted to the composables from ViewModel via LiveData or SharedFlow you defined before.
All that’s left now is to declare how MainScreen() will react to changes in state and event.
Navigate to MainScreen.kt and replace the code inside MainScreen() as follows:
// 1
val state by quizViewModel.state.observeAsState()
val event by quizViewModel.event.collectAsState(null)
// 2
when (state) {
is ScreenState.Quiz -> {
QuizScreen(
contentPadding = contentPadding,
quizViewModel = quizViewModel
)
// 3
when (val e = event) {
is Event.Error -> {
ErrorDialog(message = e.message)
}
is Event.Loading -> {
LoadingIndicator()
}
else -> {}
}
}
is ScreenState.Success -> {
ResultScreen(
contentPadding = contentPadding,
quizViewModel = quizViewModel
)
}
else -> {}
}
Add the imports where necessary. Here’s a breakdown of the code:
- It’s up to you which observable class to use for interacting with the
ViewModel. But keep in mind the differences: You useobserveAsState()to observe theLiveDataandcollectAsState()to observe theSharedFlow. You also need to set the initial state for theSharedFlow. That’s why you passnulltocollectAsState(). - Here, you instruct Jetpack Compose to recompose the UI depending on the state value.
- In this use case, you need to handle events only in
QuizScreen()and react accordingly to whether it emits anErroror aLoadingevent. Here, you assigneventto a local variable so you can use the smart cast toEventand access itsmessageproperty.
Build and run the app. Tap Try me!. You’ll see an error dialog:
Try answering the questions. If you do it incorrectly, you see an error dialog with an appropriate message. If you manage to answer all the questions, you’re redirected to the result screen.
Great, you’re almost there!
Open ResultScreen.kt and add another button to ResultScreen() right below Congrats():
SubmitButton (true, stringResource(id = R.string.start_again)) {
quizViewModel.startAgain()
}
This code block allows you to start the quiz again.
Build and run the app. When you answer all questions correctly you’ll see the result screen. There you have the button to start the quiz again.
Challenge: Adding LiveData for Questions
Now that you know how to observe variables from the ViewModel, you can make the app even prettier by adding another observable variable to QuizViewModel:
private val _questions = MutableLiveData<List<String>>()
val questions: LiveData<List<String>> = _questions
Before proceeding with the article, adjust the rest of the project and, when you’re ready, compare it with the suggested solution below.
[spoiler title="Solution"]
Modify fetchQuestions() and fetchExtendedQuestions() in QuizViewModel:
fun fetchQuestions() {
_questions.value = repository.getQuestions()
}
fun fetchExtendedQuestions() {
_questions.value = repository.getExtendedQuestions()
}
Notice that the return types and the body of these functions have changed. You don’t return the list of questions anymore. Now you use LiveData for storing the questions.
Now, add an init block to QuizViewModel:
init {
fetchQuestions()
}
Here you call fetchQuestions() while the QuizViewModel is being initialized. This way, you’ll have your LiveData‘s prepared once the app starts.
Finally, open QuizScreen.kt and find QuizScreen(). Change the questions value to look like this:
val questions by quizViewModel.questions.observeAsState()
With this line you observe questions as a State.
A few lines below, above the SubmitButton callsite, wrap the QuizInputFields() call with the let block:
questions?.let { QuizInputFields(it, onAnswerChanged) }
This is necessary because observeAsState() returns a nullable value.
In the line below, remove assigning value to questions:
quizViewModel.fetchExtendedQuestions()
You don’t need to assign the new value to questions because there fetchExtendedQuestions() doesn’t have a return type anymore.
Build and run the app.
Everything still works fine. Great job!
[/spoiler]
Observing Composable Lifecycle
You’re almost done. Pass the quiz and tap Start again to restart the quiz again. Oops, all three questions are still there.
But how do you show the initial list of just two questions again?
Of course, you could simply refresh the questions in startAgain(). But, this time you’ll try another approach.
In QuizScreen.kt, add this to the bottom of QuizScreen():
DisposableEffect(quizViewModel) {
quizViewModel.onAppear()
onDispose { quizViewModel.onDisappear() }
}
This code retrieves the initial questions at the end of the QuizScreen() lifecycle before it disappears from the screen.
Next, in QuizViewModel.kt, add the following line at the end of onDisappear():
fetchQuestions()
This code creates a side-effect. DisposableEffect() instructs QuizViewModel to call onAppear() when QuizScreen() enters the composition and call onDisappear() when it leaves the composition. This way, you can bind your ViewModel to the composable’s lifecycle so that when you move from the quiz screen to the result screen, the questions list in QuizViewModel is refreshed.
Notice that you’re using quizViewModel as a key to DisposableEffect() here, which means it will also be triggered if quizViewModel changes.
Many other side-effects can help you fine-tune the recomposition of various composables. But keep in mind you must use side-effects with caution because they will be apply even if the recomposition is canceled or restarted due to new state changes. This can lead to inconsistent UI if any of your composables depend on the side-effects.
Build and run the app. If you pass the quiz and start again, the screen looks as fresh as before.
Check out the log output. As you can see, the list of questions is recomposed before the quiz screen appears:
D/MainLog: QuizScreen disappears
D/MainLog: QuizInput What's the best programming language?
D/MainLog: QuizInput What's the best OS?
D/MainLog: Checked state false
D/MainLog: QuizScreen appears




