Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get last visible item index in jetpack compose LazyColumn

I want to check if the list is scrolled to end of the list. How ever the lazyListState does not provide this property

Why do I need this? I want to show a FAB for "scrolling to end" of the list, and hide it if last item is already visible

(Note: It does, but it's internal

  /**
   * Non-observable way of getting the last visible item index.
   */
  internal var lastVisibleItemIndexNonObservable: DataIndex = DataIndex(0)

no idea why)

val state = rememberLazyListState()
LazyColumn(
    state = state,
    modifier = modifier.fillMaxSize()
) {
    // if(state.lastVisibleItem == logs.length - 1) ...
    items(logs) { log ->
        if (log.level in viewModel.getShownLogs()) {
            LogItemScreen(log = log)
        }
    }
}

So, how can I check if my LazyColumn is scrolled to end of the dataset?

like image 603
Mahdi-Malv Avatar asked Mar 19 '21 16:03

Mahdi-Malv


3 Answers

Here is a way for you to implement it:

Extension function to check if it is scrolled to the end:

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

Example usage:

val listState = rememberLazyListState()
val listItems = (0..25).map { "Item$it" }

LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
    items(listItems) { item ->
        Text(text = item, modifier = Modifier.padding(16.dp))
    }
}

Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomEnd) {
    if (!listState.isScrolledToTheEnd()) {
        ExtendedFloatingActionButton(
            modifier = Modifier.padding(16.dp),
            text = { Text(text = "Go to Bottom") },
            onClick = { /* Scroll to the end */}
        )
    }
}

Result:

https://im.ezgif.com/tmp/ezgif-1-b725f96cedd2.gif

like image 84
pauloaap Avatar answered Oct 22 '22 02:10

pauloaap


I am sharing my solution in case it helps anyone.

It provides the info needed to implement the use case of the question and also avoids infinite recompositions by following the recommendation of https://developer.android.com/jetpack/compose/lists#control-scroll-position.

  1. Create these extension functions to calculate the info needed from the list state:
val LazyListState.isLastItemVisible: Boolean
        get() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
    
val LazyListState.isFirstItemVisible: Boolean
        get() = firstVisibleItemIndex == 0
  1. Create a simple data class to hold the information to collect:
data class ScrollContext(
    val isTop: Boolean,
    val isBottom: Boolean,
)
  1. Create this remember composable to return the previous data class.
@Composable
fun rememberScrollContext(listState: LazyListState): ScrollContext {
    val scrollContext by remember {
        derivedStateOf {
            ScrollContext(
                isTop = listState.isFirstItemVisible,
                isBottom = listState.isLastItemVisible
            )
        }
    }
    return scrollContext
}

Note that a derived state is used to avoid recompositions and improve performance. The function needs the list state to make the calculations inside the derived state. Read the link I shared above.

  1. Glue everything in your composable:
@Composable
fun CharactersList(
    state: CharactersState,
    loadNewPage: (offset: Int) -> Unit
) {
    // Important to remember the state, we need it
    val listState = rememberLazyListState()
    Box {
        LazyColumn(
            state = listState,
        ) {
            items(state.characters) { item ->
                CharacterItem(item)
            }
        }

        // We use our remember composable to get the scroll context
        val scrollContext = rememberScrollContext(listState)

        // We can do what we need, such as loading more items...
        if (scrollContext.isBottom) {
            loadNewPage(state.characters.size)
        }

        // ...or showing other elements like a text
        AnimatedVisibility(scrollContext.isBottom) {
            Text("You are in the bottom of the list")
        }

        // ...or a button to scroll up
        AnimatedVisibility(!scrollContext.isTop) {
            val coroutineScope = rememberCoroutineScope()
            Button(
                onClick = {
                    coroutineScope.launch {
                        // Animate scroll to the first item
                        listState.animateScrollToItem(index = 0)
                    }
                },
            ) {
                Icon(Icons.Rounded.ArrowUpward, contentDescription = "Go to top")
            }
        }
    }
}

Cheers!

like image 22
francosang Avatar answered Oct 22 '22 01:10

francosang


Current solution that I have found is:

LazyColumn(
    state = state,
    modifier = modifier.fillMaxSize()
) {
    if ((logs.size - 1) - state.firstVisibleItemIndex == state.layoutInfo.visibleItemsInfo.size - 1) {
        println("Last visible item is actually the last item")
        // do something
    }
    items(logs) { log ->
        if (log.level in viewModel.getShownLogs()) {
            LogItemScreen(log = log)
        }
    }
}

The statement
lastDataIndex - state.firstVisibleItemIndex == state.layoutInfo.visibleItemsInfo.size - 1
guesses the last item by subtracting last index of dataset from first visible item and checking if it's equal to visible item count

like image 38
Mahdi-Malv Avatar answered Oct 22 '22 01:10

Mahdi-Malv