Created
March 17, 2024 13:04
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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