Search code examples
androidandroid-jetpack-composelazycolumn

Maintain LazyColumn scroll position after re-composition


I'm building a composable screen, say PostScreen where multiple posts are shown in GridView and when user click on any of them, I'll navigate to DetailScreen where posts are shown in larger box with multiple buttons associated (like, comment). My logic is, when user click on any post in PostScreen, use an index from PostScreen to scroll to that index in DetailScreen. Issue is, when user click on any post (and arrive to DetailScreen), then move up (or down) wards and then click on action (for example, like a post), a coroutine operation is launched but index is getting reset and DetailScreen scroll to original index instead of staying at liked post. How would i resolve this? (I know about rememberLazyListState())

@Composable
fun DetailScreen(
    viewModel: MyViewModel,
    index: Int? // index is coming from navGraph
) {
    val postIndex by remember { mutableStateOf(index) }
    val scope = rememberCoroutineScope()
    val posts = remember(viewModel) { viewModel.posts }.collectAsLazyPagingItems()

    Scaffold(
        topBar = { MyTopBar() }
    ) { innerPadding ->
        JustNestedScreen(
            modifier = Modifier.padding(innerPadding),
            posts = posts,
            onLike = { post ->
                // This is causing index to reset, maybe due to re-composition
                scope.launch {
                    viewModel.toggleLike(
                        postId = post.postId,
                        isLiked = post.isLiked
                    )
                }
            },
            indexToScroll = postIndex
        )
    }
}

@Composable
fun JustNestedScreen(
    modifier: Modifier = Modifier,
    posts: LazyPagingItems<ExplorePost>,
    onLike: (Post) -> Unit,
    indexToScroll: Int? = null
) {
    val scope = rememberCoroutineScope()
    val listState = rememberLazyListState()

    LazyColumn(
        modifier = modifier
            .fillMaxSize()
            .background(MaterialTheme.colors.background),
        state = listState
    ) {
        items(posts) { post ->
            post?.let {
                // Display image in box and some buttons
                FeedPostItem(
                    post = it,
                    onLike = onLike,
                )
            }
        }

        indexToScroll?.let { index ->
                scope.launch {
                    listState.scrollToItem(index = index)
                }
        }
    }
}

Solution

  • Use LaunchedEffect. LaunchedEffect's block is only run the first time and then every time keys are changed. If you only want to run it once, use Unit or listState as a key:

    LaunchedEffect(listState) {
        indexToScroll?.let { index ->
            listState.scrollToItem(index = index)
        }
    }