HelloCW
HelloCW

Reputation: 2255

How can I keep expand item of a column control in Compose?

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) }
       ...
    }

Upvotes: 0

Views: 2073

Answers (2)

Arpit Shukla
Arpit Shukla

Reputation: 10493

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.

Upvotes: 1

Thracian
Thracian

Reputation: 66869

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.

Upvotes: 2

Related Questions