rememberLazyListState: Observe and Control Scroll Position

rememberLazyListState() creates the Compose-owned state for a LazyColumn or LazyRow. Use it when another UI element must react to scrolling or initiate it: a back-to-top button, a scroll-aware toolbar, or a user-triggered jump to a list item. Pass the state into the lazy layout, derive simple UI values with derivedStateOf, and use snapshotFlow for non-UI work such as analytics.

The official lazy lists guide documents firstVisibleItemIndex, firstVisibleItemScrollOffset, layoutInfo, scrollToItem(), and animateScrollToItem(). The state-saving guide notes that rememberLazyListState uses a saver to preserve scroll state across activity recreation.

Attach state to the lazy list

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable

@Composable
fun ArticleList(articles: List<ArticleUi>) {
    val listState = rememberLazyListState()

    LazyColumn(state = listState) {
        items(items = articles, key = { it.id }) { article ->
            ArticleRow(article = article, onClick = {})
        }
    }
}

Keep domain data and selection in the ViewModel. LazyListState is Compose-internal UI state, so remembering it in the composable is appropriate. Hoist it as a parameter only when a parent needs to coordinate scrolling or observe it.

Show a back-to-top action without recomposing on every pixel

firstVisibleItemScrollOffset changes continuously. For a simple Boolean UI decision, derive the value so the surrounding UI changes only when the result changes.

import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember

val listState = rememberLazyListState()
val showBackToTop by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

Use the derived value to show a button. Do not read layoutInfo directly to update composition on every scroll; its API reference warns that it updates after every scroll or remeasure and can cause unnecessary recomposition.

Scroll on a user action

scrollToItem snaps immediately. animateScrollToItem scrolls smoothly. Both are suspending functions, so launch them from an event handler.

import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch

val listState = rememberLazyListState()
val scope = rememberCoroutineScope()

Button(onClick = {
    scope.launch { listState.animateScrollToItem(index = 0) }
}) {
    Text("Back to top")
}

Avoid automatic jumps caused by ordinary recomposition. Scrolling is a visible UI effect; it should follow a clear user action or a deliberate, documented screen event.

Observe scrolling for side effects

For analytics or one-time events, use snapshotFlow, then reduce repeated signals before collecting. The official guide uses this pattern for an event after the user moves beyond the first item.

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex > 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect { analytics.trackListScrolled() }
}

Keep this effect focused on a true side effect. If the result is only needed to render a button, derivedStateOf is the better fit.

Common mistakes

  • Creating multiple states for the same list instead of passing one LazyListState.
  • Reading layoutInfo in composition on every scroll.
  • Putting app state such as selected IDs inside LazyListState.
  • Calling animateScrollToItem without a coroutine.
  • Losing identity in a changing list: combine scroll state with stable item keys.

For list content structure, spacing, and lazy item identity, see the LazyColumn guide.

FAQ

Does rememberLazyListState survive rotation?

Yes. Android’s state-saving documentation explains that it uses a saver for list scroll state across activity recreation.

When should I use snapshotFlow?

Use it for scroll-driven side effects such as analytics. For UI that depends on a coarse Boolean value, prefer derivedStateOf.

Can I jump to an item immediately?

Yes. Call scrollToItem(index) from a coroutine. Use animateScrollToItem(index) when a smooth movement is appropriate.