13

I am wondering if it is possible to get observer inside a @Compose function when the bottom of the list is reached (similar to recyclerView.canScrollVertically(1))

Thanks in advance.

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841

6 Answers6

17

you can use rememberLazyListState() and compare

scrollState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == scrollState.layoutInfo.totalItemsCount - 1

How to use example:

First add the above command as an extension (e.g., extensions.kt file):

fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1

Then use it in the following code:

@Compose
fun PostsList() {
  val scrollState = rememberLazyListState()

  LazyColumn(
    state = scrollState,),
  ) {
     ...
  }

  // observer when reached end of list
  val endOfListReached by remember {
    derivedStateOf {
      scrollState.isScrolledToEnd()
    }
  }

  // act when end of list reached
  LaunchedEffect(endOfListReached) {
    // do your stuff
  }
}

14

For me the best and the simplest solution was to add LaunchedEffect as the last item in my LazyColumn:

LazyColumn(modifier = Modifier.fillMaxSize()) {
    items(someItemList) { item ->
        MyItem(item = item)
    }
    item {
        LaunchedEffect(true) {
            //Do something when List end has been reached
        }
    }
}
  • 1
    This is the simplest and most elegant solution. Are there any drawbacks? From what I see in docs of LaunchedEffect the started coroutine will be cancelled when LaunchedEffect leaves composition. – gswierczynski Apr 22 '22 at 11:38
  • Hmm, I didn't know that. I used it to load the next items page from api to do this I just launch a new coroutine from `LaunchedEffect`(by viewModel of course) and it works well, so this is the solution if you had such a problem. Nevertheless, that's a good point – Arkadiusz Mądry Apr 27 '22 at 16:12
  • This is elegant but has a drawback when using infinite loading since the LaunchedEffect will continue to fire when you scroll past the same item in the list multiple times. Unless you invalidate it with some logic manually. – Amal May 03 '23 at 19:53
  • Another drawback is when the last item is immediately visible, e.g. when your items list is loaded from network before they are populated the last item already fires. – RufusInZen Aug 23 '23 at 12:25
7

Starting from 1.4.0-alpha03 you can use LazyListState#canScrollForward to check if you are at the end of the list.

Something like:

val state = rememberLazyListState()
val isAtBottom = !state.canScrollForward

LaunchedEffect(isAtBottom){
    if (isAtBottom) doSomething()
}

Before this release you can use the LazyListState#layoutInfo that contains information about the visible items. Note the you should use derivedStateOf to avoid redundant recompositions.

Use something:

@Composable
private fun LazyListState.isAtBottom(): Boolean {

    return remember(this) {
        derivedStateOf {
            val visibleItemsInfo = layoutInfo.visibleItemsInfo
            if (layoutInfo.totalItemsCount == 0) {
                false
            } else {
                val lastVisibleItem = visibleItemsInfo.last()
                val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset

                (lastVisibleItem.index + 1 == layoutInfo.totalItemsCount &&
                        lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight)
            }
        }
    }.value
}

The code above checks not only it the last visibile item == last index in the list but also if it is fully visible (lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight).

And then:

val state = rememberLazyListState()
var isAtBottom = state.isAtBottom()
LaunchedEffect(isAtBottom){
    if (isAtBottom) doSomething()
}

LazyColumn(
    state = state,
){
  //...
}
Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
5

I think, based on the other answer, that the best interpretation of recyclerView.canScrollVertically(1) referred to bottom scrolling is

fun LazyListState.isScrolledToTheEnd() : Boolean {
    val lastItem = layoutInfo.visibleItemsInfo.lastOrNull()
    return lastItem == null || lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset
}
dogeweb
  • 51
  • 2
  • 1
    With this solution `isScrolledToTheEnd()` is true when the bottom of the last item has been viewed, whereas the other answer only needs a part of the last item to be visible. This is important when the last item is large enough that it can't all fit in the view. – Sjolfr Nov 12 '21 at 11:31
  • I think the condition should be updated to `lastItem == null || (lastItem.index == layoutInfo.totalItemsCount - 1 && lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset)` – Nasser Ghodsian Dec 22 '22 at 11:50
0

Simply use the firstVisibleItemIndex and compare it to your last index. If it matches, you're at the end, else not. Use it as lazyListState.firstVisibleItemIndex

Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42
0

Found a much simplier solution than other answers. Get the last item index of list. Inside itemsIndexed of lazyColumn compare it to lastIndex. When the end of list is reached it triggers if statement. Code example:

LazyColumn(
        modifier = Modifier
            .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        itemsIndexed(events) { i, event  ->
            if (lastIndex == i) {
                Log.e("console log", "end of list reached $lastIndex")
            }
        }
     }
FireTr3e
  • 77
  • 7