When using a `LazyColumn` or `LazyRow`, I sometime...
# compose
m
When using a
LazyColumn
or
LazyRow
, I sometimes prepend a new item to the list. By default, if the list is already scrolled to the start, the new item ends up off-screen (above or to the left). I assume this is because Compose preserves the currently visible item (what was index 0 is now index 1) rather than preserving the scroll position. What I'd like instead is: if the user is already at scroll position 0, then after prepending an item, keep the scroll position at 0 so the new first item is immediately visible. (If the user has scrolled away from the start, I want the default behavior of preserving the visible content.) Is there a built-in way to achieve this, or is manually calling
scrollToItem(0)
after detecting that the list was at the start before the update the recommended approach?
r
No, there’s no built-in way. The usual approach is to detect if the list was already at the top before prepending, and if so, call
scrollToItem(0)
(or
animateScrollToItem(0)
) after the update. Otherwise, let Compose preserve the current position.
đŸ§µ 3
m
I ended up using this:
Copy code
val firstItemKey = items.firstOrNull()?.first
val wasAtStart = remember(firstItemKey) {
    state.firstVisibleItemIndex == 0 && state.firstVisibleItemScrollOffset == 0
}
LaunchedEffect(firstItemKey) {
    if (wasAtStart && items.isNotEmpty()) {
        state.animateScrollToItem(0, 0)
    }
}
r
That’s a neat approach. Thanks for sharing!
s
I'm a little late on responding to this. I wrote a client with scrolling text. The method you mentioned is brittle and can lose its place either by resizing or by an element with 0 size. First is my original scrolling down, then reversed scrolling up.
Copy code
var sticky by remember { mutableStateOf(true) }
    val lastSerial = lines.lastOrNull()?.serialNumber
    LaunchedEffect(lastSerial) {
        if (lastSerial != null && sticky) {
            lines.lastIndex.takeIf { it > -1 }?.let { index ->
                scrollState.scrollToItem(index)
            }
        }
    }
    LaunchedEffect(scrollState.lastScrolledBackward, scrollState.canScrollForward) {
        // If the user scrolled back and they can scroll forward, stop being sticky
        // if the window can't be scrolled forward, be sticky again
        if (scrollState.lastScrolledBackward && scrollState.canScrollForward) {
            sticky = false
        } else if (!scrollState.canScrollForward) {
            sticky = true
        }
    }
untested reversal:
Copy code
var sticky by remember { mutableStateOf(true) }
    LaunchedEffect(firstItemKey) {
        if (sticky && items.isNotEmpty()) {
            scrollState.animateScrollToItem(0, 0)
        }
    }
    LaunchedEffect(scrollState.lastScrolledBackward, scrollState.canScrollForward) {
        // If the user scrolled back and they can scroll forward, stop being sticky
        // if the window can't be scrolled forward, be sticky again
        if (scrollState.lastScrolledForward && scrollState.canScrollBackward) {
            sticky = false
        } else if (!scrollState.canScrollBackwaard) {
            sticky = true
        }
    }