0

The Code A is from the official sample code here.

And author told me the following content:

If you expand item number 1, you scroll away to number 20 and come back to 1, you'll notice that 1 is back to the original size.

Question 1: Why will the expand item be restored to original size after scroll forward then backward items with Code A?

Question 2: How can I keep expand item after scroll forward then backward items ?

Code A

@Composable
private fun Greetings(names: List<String> = List(1000) { "$it" } ) {
    LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Composable
private fun Greeting(name: String) {    
    var expanded by remember { mutableStateOf(false) }    
    val extraPadding by animateDpAsState(                
        if (expanded) 48.dp else 0.dp
    ) 

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

Added Content

Question 3: If I use Code B, I find the expand items can be kept after I scroll forward then backward items. why ?

Code B

@Composable
private fun Greetings(names: List<String> = List(50) { "$it" } ) {
    Column(
        modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
    )
    {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

... 

Again:

I rewrite Code C by jVCODE's suggestion, it works, the expand items can be kept after I scroll forward then backward items after I replace var expanded by remember { mutableStateOf(false) } with var expanded by rememberSaveable { mutableStateOf(false) }.

According to Arpit Shukla's view point:

When you scroll in LazyColumn, the composables that are no longer visible get removed from the composition tree and when you scroll back to them, they are composed again from scratch. That is why expanded is initialized to false again.

In my mind, the rememberSaveable will only be available when I rotate screen.

So I think var expanded by rememberSaveable { mutableStateOf(false) } will be relaunched and assigned as false when I scroll forward then backward items, and the expand items will be restored to original size. But in fact the expand items can be kept after I scroll forward then backward items.

Question 4: Why can rememberSaveable work well in this scenarios?

Code C

  @Composable
    private fun Greetings(names: List<String> = List(1000) { "$it" } ) {
        LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {
            items(items = names) { name ->
                Greeting(name = name)
            }
        }
    }

    @Composable
    private fun Greeting(name: String) {    
      var expanded by rememberSaveable { mutableStateOf(false) }
       ...
    }
HelloCW
  • 843
  • 22
  • 125
  • 310

2 Answers2

2

1- When a Composable enters composition the value in remember is initialized. With LazyColumn when composables are not visible that are not visible removed from composition three and when you scroll back to same item again the value in remember{..} is initialized.

enter image description here

2- You can create a UI version of your model class and a extended property to your data class. You can

data class UiModel(
    ...
    var expanded: Boolean = false
)

This way you can store extended state in your ViewModel.

    LazyColumn(

        content = {

            items(tutorialList) { item: UiModel ->

                var isExpanded by remember(key1 = item.title) { mutableStateOf(item.expanded) }

                UiModel(
                    model = item,
                    onExpandClicked = {
                        item.expanded = !item.expanded
                        isExpanded = item.expanded
                    },
                    expanded = isExpanded
                )
            }
        }
    )

Result is

enter image description here

Edit

LazyColumn and LazyRow use a layout called SubComposeLayout, you can read detailed answer from Google developer here, it lazily loads like RecyclerView does. A good article, Under the hood of Jetpack Compose, by Compose Runtime developer Leland Richardson is a good source to help understand what's going under the hood.

When a composable removed removed from composition depends on architecture, for example simple loading and displaying result in a function such as

@Composable fun App() {
 val result = getData()
 if (result == null) {
   Loading(...)
 } else {
   Header(result)
   Body(result)
 }
}

is actually compiled as

fun App($composer: Composer) {
 val result = getData()
 if (result == null) {
   $composer.start(123)
   Loading(...)
   $composer.end()
 } else {
   $composer.start(456)
   Header(result)
   Body(result)
   $composer.end()
 }
}

You can check other details from the medium article about composition for the snippet from the medium link shared.

An easy way to track when a Composable enters and exits composition is using DisposableEffect function

It represents a side effect of the composition lifecycle.

  • Fired the first time (when composable enters composition) and then every time its keys change.
  • Requires onDispose callback at the end. It is disposed when the composable leaves the composition, and also on every recomposition when its keys have changed. In that case, the effect is disposed and relaunched.
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Thanks! will any composable always be removed from the composition tree when they are no longer visible? – HelloCW Nov 14 '21 at 01:53
  • Thanks! Would you please to see my added content in the question? If I use Code B, I find the expand items can be kept after I scroll forward then backward items. why ? – HelloCW Nov 14 '21 at 02:31
  • It does not depend on visibility it depends on your composable architecture. There is a SideEffect function on `DisposableEffect`which fired the first time (when composable enters composition) and then every time its keys change. and `onDispose` when it's removed from composition. You can check out with that. And there is a `StaggeredGrid` implementation in [Codelab page](https://developer.android.com/codelabs/jetpack-compose-layouts#7) if you inspect it you will see that items that are not visible but still in composition. – Thracian Nov 14 '21 at 07:45
1

var expanded by remember { mutableStateOf(false) }
This expanded state is local to the Greeting composable. When you scroll in LazyColumn, the composables that are no longer visible get removed from the composition tree and when you scroll back to them, they are composed again from scratch. That is why expanded is initialized to false again.

If you want to preserve the expanded state, you should hoist this state in a different state holder (for example in the list where you store the names, also store the expanded state of the Greeting with that name). And if you also want this to survive configuration changes you should hoist this state from a ViewModel.

You can also use rememberSaveable to save the expanded state but if you have a very large list, you will end up saving a lot of data in the bundle. So I would suggest avoid using rememberSaveable inside a LazyColumn.

Arpit Shukla
  • 9,612
  • 1
  • 14
  • 40
  • 1
    Wouldn't replacing `remember` with `rememberSaveable { .. }` solve the issue as well, without the need to involve a ViewModel? – VCODE Nov 13 '21 at 19:14
  • Thanks! will any composable always be removed from the composition tree when they are no longer visible? – HelloCW Nov 14 '21 at 01:54
  • Thanks! Would you please to see my added content in the question? If I use Code B, I find the expand items can be kept after I scroll forward then backward items. why ? – HelloCW Nov 14 '21 at 02:31
  • 1. LazyColumn might decide to keep 1 or 2 items in the tree just above the first visible item and below the last visible item. But you can't (and need not) predict, it's just those items will be removed when you scroll away from them. 2. Code B works because you are not using LazyColumn there, a normal Column always keeps all the composables in the composition tree (and that's why this isn't suited for large lists of data) – Arpit Shukla Nov 14 '21 at 03:12
  • Thanks! Why can rememberSaveable work well in this scenarios in Code C ? – HelloCW Nov 14 '21 at 03:15
  • 1
    Because when you use `rememberSaveable`, the data is not saved in the composition tree. Instead it is saved using a `SaveableStateRegistry` (which uses the saved instance state mechanism), that's the reason it survives configuration change and process recreation. – Arpit Shukla Nov 14 '21 at 03:28