Skip to content

Instantly share code, notes, and snippets.

@flaringapp
Created March 17, 2024 13:04
Show Gist options
  • Save flaringapp/36d241c802fe3e819e46a4ef63794a1b to your computer and use it in GitHub Desktop.
Save flaringapp/36d241c802fe3e819e46a4ef63794a1b to your computer and use it in GitHub Desktop.
Implements mechanism for LazyList pagination callback. Also supports disabling (e.g. while loading) and throttling callbacks.
private const val DEFAULT_THROTTLE_TIMEOUT = 500L
@Composable
fun LazyListPagination(
state: LazyListState,
enable: Boolean,
onPaginate: () -> Unit,
offset: Int = 0,
throttleTimeout: Long = DEFAULT_THROTTLE_TIMEOUT,
) {
require(throttleTimeout > 0) {
"LazyList pagination listener requires `throttleTimeout` > 0!"
}
BasicLazyListPagination(
state = state,
offset = offset,
) {
if (!enable) return@BasicLazyListPagination
onPaginate()
delay(throttleTimeout)
}
}
/**
* Core pagination listener that invokes [onPaginate] every time a unique pagination condition
* is achieved for given [state] within [offset].
*
* Suspending [onPaginate] also suspends new triggers detection, but last new trigger will be
* processed after [onPaginate] is completed.
*
* @see paginationTriggerOf
* @see PaginationTrigger
*/
@Composable
private fun BasicLazyListPagination(
state: LazyListState,
offset: Int,
onPaginate: suspend (PaginationTrigger) -> Unit,
) {
require(offset >= 0) {
"LazyList pagination listener requires `offset` >= 0!"
}
val currentOffset by rememberUpdatedState(offset)
val currentOnPaginate by rememberUpdatedState(onPaginate)
LaunchedEffect(state) {
// Behaves similar to conflated channel: compose state changes polling is suspended while
// downstream collector is suspended
snapshotFlow {
paginationTriggerOf(
layoutInfo = state.layoutInfo,
offset = currentOffset,
)
}
// Skip null values as they indicate leaving pagination offset
.filterNotNull()
.collect {
currentOnPaginate(it)
}
}
}
/**
* Instantiates [PaginationTrigger] with current [layoutInfo] and [offset]. Returns `null` if
* last visible item is outside offset bounds.
* @see lastVisibleItemIndexWithinOffset
* @see PaginationTrigger
*/
private fun paginationTriggerOf(
layoutInfo: LazyListLayoutInfo,
offset: Int,
): PaginationTrigger? {
val paginationIndex = layoutInfo.lastVisibleItemIndexWithinOffset(offset) ?: return null
return PaginationTrigger(
itemIndex = paginationIndex,
totalItemsCount = layoutInfo.totalItemsCount,
)
}
/**
* Returns index of last visible item within given [offset] from the end of list or `null`.
*
* E.g. given list with size `n`. If current last visible item index `i` is within
* offset bounds `n - offset - 1 <= i <= n - 1`, then function returns index `i`. If index `i` is
* outside offset bounds `0 <= i < n - offset - 1`, then function returns `null`.
*/
private fun LazyListLayoutInfo.lastVisibleItemIndexWithinOffset(offset: Int): Int? {
val lastVisibleItemIndex = visibleItemsInfo.lastOrNull()?.index ?: return null
val lastItemIndex = totalItemsCount - 1
return lastVisibleItemIndex.takeIf { it >= (lastItemIndex - offset) }
}
/**
* A class used to distinguish unique pagination triggering layout states.
*/
private data class PaginationTrigger(
val itemIndex: Int,
val totalItemsCount: Int,
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment