2

I'm trying to understand the lifecycle of composable in jetpack compose. It is written in the docs:

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

enter image description here

But I don't really get the meaning of entering and leaving the composition. Can we roughly assume that at any moment of time, each composable that is shown on the screen is "in the composition"? Let me explain it with an example from the docs. Consider the following code snippet:

@Composable fun LoginScreen(showError: Boolean) { if (showError) { LoginError() } LoginInput() } @Composable fun LoginInput() { /* ... */ } @Composable fun LoginError() { /* ... */ } 

If we pass false to the LoginScreen() is LoginError() out of the composition? and if the state changes and the we pass true to LoginScreen() will LoginError() enters the composition?

2
  • 2
    Yes and yes. It's a reactive paradigm: the UI will only get recomposed when a state reactively changes. Commented Sep 10, 2023 at 15:03
  • 1
    IMHO, you have the right idea at a high level. "On the screen" may or may not be accurate depending on the container -- for example, a LazyColumn() might have some nodes that are off-screen but ready to be used when the user starts scrolling. But, if we assume in this case that all the composables from LoginScreen() up to the root do not do this, then enter/leave will generally align with "included in the UI"/"removed from the UI". Commented Sep 10, 2023 at 15:04

2 Answers 2

1

Composition can be described as how nodes of composition tree are structured. They don't have to be necessarily be ui Composables either and they don't have to be drawn on screen to be part of composition.

Your example is correct any conditional statement can change how it will be structured on recomposition.

Composition is first time a Composable takes place in composition tree, when any State is read any part of scope of that Composable or whole Composable is updated with new value, based on some other factors it might be updated with same value as well such as SnapshotMutationPolicy or stability of Composable, this is recomposition. When some logic removes a Composable from the tree that Composable leaves composition.

DisposableEffect function is a non ui Composable that you can track when a Composable enters or exits composition.

In example below Text and DisposableEffect enters recomposition when counter is 3, stays in composition and Text is recomposed with new counter value until 6 and when it reaches 6 DisposableEffect and Text leaves composition

@Preview @Composable private fun UIComposableSample() { val context = LocalContext.current var counter by remember { mutableStateOf(0) } if (counter in 3..5) { Text("Counter: $counter") DisposableEffect(Unit){ Toast.makeText(context, "enter recomposition $counter", Toast.LENGTH_SHORT).show() onDispose { Toast.makeText(context, "exit recomposition $counter", Toast.LENGTH_SHORT).show() } } } Button(onClick = { counter++ }) { Text("Counter: $counter") } } 

And there is no need to be ui composable for enter, recomposition, leave composition lifecycle either.

@Composable private fun NonUIComposableSample() { val context = LocalContext.current var counter by remember { mutableStateOf(0) } var color by remember { mutableStateOf(Color.Red) } if (counter in 3..5) { DisposableEffect(Unit) { Toast.makeText(context, "Entering Composition counter: $counter", Toast.LENGTH_SHORT).show() color = Color.Yellow onDispose { color = Color.Green Toast.makeText(context, "Exiting Composition counter: $counter", Toast.LENGTH_SHORT).show() } } } Button(onClick = { counter++ }) { Text("Counter: $counter", color = color) } } 

or this one LaunchedEffect enter composition when counter is bigger than 0 and multiply of 3 LaunchedEffect enters composition and Snackbar is shown if you press Button and counter increases Snackbar stops is removed otherwise it waits until it's time out to fade out and leave composition.

/** * Adding this LaunchedEffect to composition when condition is true * Same goes for if this was remember(LaunchedEffect under the hood uses `remember(key){}` * when condition is met remember gets added to composition and it gets removed when it's not met */ @Composable private fun LaunchedEffectExample(scaffoldState: ScaffoldState) { var counter by remember { mutableStateOf(0) } if (counter > 0 && counter % 3 == 0) { // `LaunchedEffect` will cancel and re-launch if // `scaffoldState.snackbarHostState` changes LaunchedEffect(scaffoldState.snackbarHostState) { // Show snackbar using a coroutine, when the coroutine is cancelled the // snackbar will automatically dismiss. This coroutine will cancel whenever // if statement is false, and only start when statement is true // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes. scaffoldState.snackbarHostState.showSnackbar("LaunchedEffect snackbar") } } // This button increase counter that will trigger LaunchedEffect OutlinedButton( modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), onClick = { counter++ } ) { Text("LaunchedEffect Counter $counter") } } 

And for Ui Composable there are three phases alongside with composition which is invoked in depth three trevesal.

Compose has three main phases:

Composition: What UI to show. Compose runs composable functions and creates a description of your UI.

Layout: Where to place UI. This phase consists of two steps: measurement and placement. Layout elements measure and place themselves and any child elements in 2D coordinates, for each node in the layout tree.

Drawing: How it renders. UI elements draw into a Canvas, usually a device screen.

Even if a Composasble has 0.dp size or not drawn on screen(if you assign Modifier.drawWithContent{} not draw content), or even if it's not or placed( if you don't call place in placement scope) it still might be part of composition tree and means that it's composed.

Let's say you have ui tree like the one below

Column() { println("Parent Scope") Column() { println("Child1 Outer Scope") Text("Child1 Outer Content") Column() { println("Child1 Inner Scope") Text("Child1 Inner Content") } } Column() { println("Child2 Scope") Text("Child2 Content") } } 

Composition creates nodes such as like the one below

 Parent Layout / \ / \ / \ / \ Child1 Outer Child2 | Child1 Inner 

Prints

 I Parent Scope I Child1 Outer Scope I Child1 Inner Scope I Child2 Scope 

This is the order of Composition happening starts tree from left goes until inner child, goes to next branch and so on. Then in Layout phase child1 Inner gets measured first, Child1 Outer, Child2 then Parent gets measured with info from child and they get place same order Composition happens.

If the Composables are like this

 Parent Composable / | \ / | \ / | \ / | \ / | \ C1 Outer C2Outer C3outer / | \ / / \ \ / / \ \ / / \ \ C1 Middle C2 InnerA C2 InnerB C3 Inner | C1 Inner Prints: I Paren Scope I Child1 Outer Scope I Child1 Middle Scope I Child1 Inner Scope I Child2 Outer Scope I Child2 InnerA Scope I Child2 InnerB Scope I Child3 Outer Scope I Child3 Inner Scope 

This is how they are composed in composition. On recomposition based on which composable reads a value only it or one its parent or entire composition tree might get recomposed.

https://stackoverflow.com/a/73181512/5457853

https://stackoverflow.com/a/73181512/5457853

https://stackoverflow.com/a/77007725/5457853

Sign up to request clarification or add additional context in comments.

Comments

0

In Jetpack Compose, understanding the lifecycle of composables and when they are "in the composition" can be a bit nuanced, so let's clarify it: 1. Composition: A composition is essentially a tree of composables that represents your UI. When you build a Composable function (like LoginScreen), you are creating a node in this tree. Each composable function defines a part of your UI, and the entire UI is a composition of these composable functions. 2. Entering and Leaving the Composition: The concept of "entering" and "leaving" the composition usually refers to whether a composable function is currently part of the composition tree or not. Now, let's look at your example:

 @Composable fun LoginScreen(showError: Boolean) { if (showError) { LoginError() } LoginInput() } @Composable fun LoginInput() { /* ... */ } @Composable fun LoginError() { /* ... */ } 

In this example, LoginScreen is the root of a composition. It calls LoginInput and conditionally calls LoginError based on the value of showError. - If you pass false to LoginScreen(), the LoginError() composable will not be part of the composition. It won't be included in the composition tree, and it won't be displayed on the screen. - If you pass true to LoginScreen(), the LoginError() composable will be included in the composition. It will enter the composition tree, and its UI will be displayed on the screen. So, yes, you can roughly assume that at any moment, each composable that is shown on the screen is "in the composition." When you change the state and re-compose your UI (e.g., by changing showError from false to true), Jetpack Compose will automatically update the composition to reflect the changes in your state, which may involve adding or removing composables from the composition tree as needed.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.