Android Jetpack Compose — Part 2 : Lifecycle of composables

Guruprasad Hegde
8 min readAug 30, 2024

--

In Jetpack Compose, understanding the lifecycle of composable functions is crucial for building efficient and responsive UI. The lifecycle of composable functions is somewhat different from the traditional Android View system, as it’s driven by the declarative nature of Compose.

The lifecycle of a composable is defined by the following events: entering the Composition, getting recomposed 0 or more times, and leaving the Composition.

A Composition can only be produced by an initial composition and updated by recomposition. The only way to modify a Composition is through recomposition.

1. Initial Composition

  • During the initial composition, the composable function is executed for the first time. This is where the UI is laid out, and the elements are drawn on the screen. At this point, Compose builds a UI tree by calling each composable function and constructing the necessary UI elements based on the state and data provided.
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}

@Composable
fun MyScreen() {
// Initial Composition
Greeting(name = "Compose")
}

When Greeting("John") is called for the first time, Compose creates a Text element displaying "Hello, John!" on the screen. This is the initial composition.

Key Points:

  • The function executes from top to bottom.
  • UI elements (like Text, Button) are created based on the code within the composable.
  • Compose tracks the state variables used within this function.

2. Recomposition

  • Recomposition is triggered when the state or data that a composable function depends on changes. Compose intelligently re-executes the composable functions, but only the parts of the UI affected by the change are updated. This ensures the UI reflects the current state of the app.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }

Button(onClick = { count++ }) {
Text(text = "Clicked $count times")
}
}

Each time the button is clicked, count is incremented, and the Text composable is re-executed to display the updated count. The button itself doesn’t change, so it is not re-executed.

Key Points:

  • Not the entire composable is re-executed; only the parts affected by the state change are.
  • This happens dynamically when state variables that the composable depends on are updated.
  • The recomposition is efficient, avoiding unnecessary work by skipping parts of the UI that haven’t changed.

3. Skipping Recomposition

  • Compose optimizes performance by skipping recomposition when it determines that the output of a composable function wouldn’t change, even if it’s re-executed. This optimization avoids unnecessary work and makes your UI more efficient.
@Composable
fun StaticText() {
Text(text = "This is a static text")
}

If the state changes elsewhere in the app but doesn’t affect StaticText, Compose skips recomposition for this function. The text remains unchanged, so there’s no need to re-execute the composable.

Key Points:

  • Skipping is determined by checking whether the output of the composable would change with the new state.
  • Skipped recompositions do not re-render any UI elements.

4. Disposal

  • When a composable is no longer needed — like when a user navigates away from a screen or a condition controlling visibility changes — Compose disposes of the UI elements. This phase is crucial for resource management and preventing memory leaks.
@Composable
fun Timer() {
DisposableEffect(Unit) {
val timer = Timer()
timer.scheduleAtFixedRate(0, 1000) {
// Update UI every second
}
onDispose {
timer.cancel()
}
}
}

When the composable that uses this timer is removed from the UI, onDispose is called, cancelling the timer and freeing up resources.

Key Points:

  • Use DisposableEffect to handle any cleanup tasks, like canceling a network request or stopping a timer.
  • Disposal ensures that your app doesn’t hold onto resources longer than necessary, preventing memory leaks.

How does Jetpack Compose handle the lifecycle of composables that are called multiple times within a Composition?

When a composable is called multiple times within a Composition, each instance is treated as a separate entity with its own lifecycle. This means that each call to the composable creates a distinct instance that Compose tracks independently. Each instance can have its own state, and Compose manages the recomposition and state updates for each one individually.

Let’s create a simple composable that displays an image and a counter. We’ll then call this composable multiple times in a list. Each instance will have its own state and lifecycle.

package com.example.jetpackcompose

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.jetpackcompose.ui.theme.JetpackComposeTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
JetpackComposeTheme {

Column(
modifier = Modifier
.fillMaxWidth()
.height(2160.dp)
.padding(top = 30.dp)
) {
ImageWithCounter(
imageSource = R.drawable.baseline_verified_user_24,
description = "Image 1"
)

ImageWithCounter(
imageSource = R.drawable.baseline_heart_broken_24,
description = "Image 2"
)

ImageWithCounter(
imageSource = R.drawable.baseline_verified_user_24,
description = "Image 3"
)
}
}
}
}
}

@Composable
fun ImageWithCounter(modifier: Modifier = Modifier, imageSource: Int, description: String) {

var clickCount by remember {
mutableIntStateOf(0)
}

Card(
modifier
.fillMaxWidth()
.height(120.dp)
.padding(10.dp),
border = BorderStroke(1.dp, Color.Blue)
) {
Row(
modifier
.fillMaxWidth()
.padding(5.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = imageSource),
contentDescription = description,
modifier = Modifier.size(64.dp)
)

Column(
modifier.padding(top = 10.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "You clicked $clickCount times", style = TextStyle(
fontSize = 14.sp,
color = Color.Black,
fontWeight = FontWeight.Bold
)
)

Spacer(modifier = Modifier.height(10.dp))
Button(onClick = {
clickCount++
}) {
Text(text = "Click me")
}
}
}
}
}

@Preview(showBackground = true, showSystemUi = true)
@Composable
private fun ImageWithCountPreview() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 30.dp)
) {
ImageWithCounter(
imageSource = R.drawable.baseline_verified_user_24,
description = "Image 1"
)

ImageWithCounter(
imageSource = R.drawable.baseline_dehaze_24,
description = "Image 2"
)

ImageWithCounter(
imageSource = R.drawable.baseline_add_ic_call_24,
description = "Image 3"
)
}
}

Output of the program:

1. Independent Instances:

  • Each time a composable(ImageWithCounter) is called, even if it’s the same function, Compose creates a new instance in the Composition.
  • These instances are tracked independently, meaning they can each have their own state and lifecycle.

2. State Management:

  • Each instance of the composable can maintain its own state using remember and State objects.
  • Changes to the state in one instance do not affect other instances, even if they are created from the same composable function.

3. Recomposition:

  • When the state of an instance changes, only that specific instance is recomposed. Other instances remain unchanged unless their state changes as well.
  • Compose efficiently updates the UI by only recomposing the affected instance, not the entire Composition.

4. Lifecycle Events:

  • Each composable instance goes through its own lifecycle events like onCreate(Initialization), onUpdate (Recomposition), and onDispose(Leaving composition).
  • These events are handled separately for each instance, allowing them to be created, updated, and disposed of independently.

How do composable functions in Jetpack Compose interact with the Activity lifecycle during their own lifecycle within the Composition?

Jetpack Compose composables are designed to be lifecycle-aware, but they do not directly tie into the traditional Android Activity lifecycle (like onCreate, onStart, onResume, etc.). Instead, they have their own lifecycle within the Composition and use the LifecycleOwner from the Activity or Fragment context when necessary.

However, composables can be made aware of the Activity lifecycle events (ON_START, ON_STOP, etc.) by using lifecycle-aware components like LifecycleObserver or DisposableEffect with LocalLifecycleOwner. This allows composables to perform actions when these activity-level events occur, but these are not composable lifecycle events themselves.

Relationship Between Composable Lifecycle and Activity Lifecycle

  1. Composable Lifecycle:
  • Composables have their own lifecycle, defined by when they are added to the Composition, recomposed, and removed from the Composition.
  • Key lifecycle events include entering the Composition (initialization), recomposition (updating), and leaving the Composition (disposal).

2. Activity Lifecycle:

  • The traditional Android lifecycle methods (onCreate, onStart, onResume, etc.) still exist and are tied to the overall lifecycle of the Activity or Fragment hosting the Compose UI.

3. Integration:

When a composable function needs to interact with the Activity or Fragment lifecycle (e.g., for managing resources that depend on the lifecycle), it can do so using lifecycle-aware APIs provided by Jetpack Compose.

Compose automatically provides access to the LifecycleOwner of the surrounding Activity or Fragment, allowing composables to be aware of and react to lifecycle events.

package com.example.jetpackcompose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver

@Composable
fun LifecycleAwareComposable() {
val lifecycleOwner = LocalLifecycleOwner.current
println("Compose Lifecycle ${lifecycleOwner.lifecycle.currentState.name}")

DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { source, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> {
// Handle ON_CREATE event
println("Activity created")
}

Lifecycle.Event.ON_START -> {
// Handle ON_START event
println("Activity started")
}

Lifecycle.Event.ON_RESUME -> {
// Handle ON_RESUME event
println("Activity resumed")
}

Lifecycle.Event.ON_PAUSE -> {
// Handle ON_PAUSE event
println("Activity paused")
}

Lifecycle.Event.ON_STOP -> {
// Handle ON_STOP event
println("Activity stopped")
}

Lifecycle.Event.ON_DESTROY -> {
// Handle ON_DESTROY event
println("Activity destroyed")
}

else -> {}
}
}

lifecycleOwner.lifecycle.addObserver(observer)

onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
  • LocalLifecycleOwner.current: This provides the current LifecycleOwner (typically the Activity or Fragment that hosts the Compose UI).
  • DisposableEffect: This is used to manage the lifecycle of the observer, ensuring it is added when the composable enters the Composition and removed when it leaves.
  • LifecycleEventObserver: This observes lifecycle events like ON_START and ON_STOP, allowing the composable to react accordingly.

The role of ‘ remember’ in managing state during the lifecycle of a composable function.

In Jetpack Compose, remember plays a crucial role in managing state during the lifecycle of a composable function. It allows you to retain and manage state across recompositions, which is essential for creating responsive and dynamic UIs.

Understanding remember

remember is a composable function that stores an object in memory across recompositions. When you use remember, the stored value is preserved even when the composable function is re-executed due to state changes, avoiding the need to recreate the value every time.

  • During the initial composition, when a composable function is executed for the first time, remember initializes and stores the value. This value is kept in memory, ensuring that it is not lost when the function is recomposed.
@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }

Button(onClick = { count.value++ }) {
Text("Clicked ${count.value} times")
}
}
  • During the recomposition, When the state or data in the composable changes, triggering a recomposition, remember ensures that the state is preserved. This means the value stored by remember remains intact and is reused during the recomposition.

Without remember:

  • If remember were not used, the count would be reset to its initial value (0) every time the composable function is recomposed, effectively making the UI unresponsive to user interactions.

With remember:

  • The count variable retains its value across recompositions, allowing the UI to reflect the current state correctly.
  • In cases where recomposition is skipped, the value stored in remember is unaffected. This further enhances performance by ensuring that only the necessary parts of the UI are recomposed, while the state managed by remember remains stable.
  • When a composable is disposed of, the state stored by remember is also discarded. This means that when the composable is removed from the composition (e.g., when navigating away from a screen), the memory allocated by remember is freed up.

How does Jetpack Compose handle configuration changes like screen rotations, and how can UI state be preserved?

Jetpack Compose has a smart way of handling configuration changes (like screen rotations or changes in dark mode) that simplifies development and improves the user experience.

  • Configuration Changes as State: Compose treats configuration changes like any other state change. When a configuration change occurs, Compose triggers recomposition of the affected composables.
  • No Activity Recreation: Unlike traditional Android development, Compose doesn’t recreate the entire Activity on configuration changes. This leads to smoother transitions and a better user experience. State Preservation
  • rememberSaveable: Use rememberSaveable to store state that should persist across configuration changes. This is similar to using onSaveInstanceState in traditional Android development.

Happy Coding !!

--

--