Remember list item states while navigating in Jetpack Compose

If we create state for a list item like val state = remember(it) { mutableStateOf(ItemState()) } then we lose expanded state while scrolling.

If we move state generation up higher before LazyColumn then expand state saves properly

 val states = items.map { remember(it) { mutableStateOf(ItemState()) } }
 LazyColumn(modifier = Modifier.fillMaxSize()) {....

But when we expand an item, click the button, go to the details screen and then go back to items we lose expanded state.
What is the best way to save items' state?

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            RememberStateTheme {
                val navController = rememberNavController()

                NavHost(navController, startDestination = "items") {
                    composable("items") {
                        Greeting(
                            onItemClick = { navController.navigate("details/$it") }
                        )
                    }
                    composable(
                        "details/{index}",
                        arguments = listOf(navArgument("index") { type = NavType.IntType })
                    ) { backStackEntry ->
                        DetailsScreen(backStackEntry.arguments?.getInt("index") ?: -1)
                    }
                }
            }
        }
    }
}

@Composable
fun Greeting(
    onItemClick: (Int) -> Unit
) {
    val items = remember { (0..100).toList() }

    Surface(color = MaterialTheme.colors.background) {
        val states = items.map { remember(it) { mutableStateOf(ItemState()) } }

        LazyColumn(modifier = Modifier.fillMaxSize()) {
            items(items) { item ->
                // If we create state here like val state = remember(it) { mutableStateOf(ItemState()) }
                // then we loose expanded state while scrolling
                // If we lift up states generating higher before LazyColumn then expand state
                // is saving properly
                //
                // But when we expand an item, click the button and then go back to items we loose
                // expanded state
                val state = states[item]

                key(item) {
                    Item(index = item,
                        state = state.value,
                        onClick = { onItemClick(item) },
                        modifier = Modifier
                            .fillMaxSize()
                            .clickable {
                                state.value.changeState()
                            }
                    )
                }
                Divider()
            }
        }
    }
}

@Composable
fun Item(
    index: Int,
    state: ItemState,
    modifier: Modifier = Modifier,
    onClick: () -> Unit
) {
    Box(modifier = modifier) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier.align(Alignment.Center)
        ) {
            Text(
                text = index.toString(),
                modifier = Modifier.padding(16.dp)

            )

            if (state.expanded) {
                Button(
                    onClick = onClick,
                    modifier = Modifier.padding(8.dp)
                ) {
                    Text(text = "Click me")
                }
            }
        }
    }
}

class ItemState {

    val expanded: Boolean
        get() = _expanded.value

    private val _expanded = mutableStateOf(false)

    fun changeState() {
        _expanded.value = !_expanded.value
    }
}

@Composable
fun DetailsScreen(
    index: Int,
    modifier: Modifier = Modifier
) {
    Box(
        modifier = modifier
            .fillMaxSize()
            .background(Color.Gray.copy(alpha = 0.3f))
    ) {
        Text(
            text = index.toString(),
            modifier = Modifier.align(Alignment.Center)
        )
    }
}

Upvotes: 9

Views: 17126

Answers (2)

Using rememberSaveable solves the problem.

Thanks to https://stackoverflow.com/users/1424349/leland-richardson

    LazyColumn(modifier = Modifier.fillMaxSize()) {
        items(items) { item ->
            val state = rememberSaveable(item) { mutableStateOf(ItemState()) }
    
            key(item) {
                Item(index = item,
                    state = state.value,
                    onClick = { onItemClick(item) },
                    modifier = Modifier
                        .fillMaxSize()
                        .clickable {
                            state.value.changeState()
                        }
                )
            }
            Divider()
        }
    }

Upvotes: 21

CommonsWare
CommonsWare

Reputation: 1007464

But when we expand an item, click the button, go to details screen and then go back to items we loose expanded state.

Correct. remember() remembers for the scope of a specific composition. This means it remembers across recompositions, but not when the composition is replaced by a separate composition. In your case, navigation replaces your Greeting() composition with a DetailsScreen() composition.

What is the best way to save items state?

Hoist the state further, to a composition that does not get replaced by navigation. In this case, that would be your root composition, where you have your rememberNavController() call.

Or, have the state be stored in a viewmodel that is scoped to your activity, or at least to that root composition.

If you want to have this state persist beyond the life of your process, I think that the vision is that we should use effects to save the state via a repository to some persistent store (e.g., JSON file) and restore the state from that store. However, I have not experimented with this approach yet.

Upvotes: 6

Related Questions