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.

3 (2) · 2 Reviews

Download materials
Save for later
Share

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.
Note: Recomposition is tightly connected with state in Jetpack Compose. It’s best to go through this tutorial after familiarizing yourself with that concept. Consider starting with Managing State in Compose if you haven’t done so yet.

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.

Quiz screen input with immutable string

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

Lifecycle of Composable

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.

Note: If you’re wondering why there are no setters and getters in composable functions, you can learn more about it in Jetpack Compose by Tutorials — Updated for the Compose Beta!

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.

Quiz input screen with input state

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:

  1. It receives a Boolean checked state and an onChange 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.
  2. You assigned the value and lambda to the Checkbox component. Now, if the user changes the checked state, the component reacts with the onChange lambda, lifting the current state of isChecked to QuizScreen().

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.

Trigger recomposition from 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.