Adaptive Paging
The following guide helps create the UI/UX seen below:
Guide
Situations can arise that can require more dynamic pagination. Consider paging for an adaptive layout. The items fetched can be a function of:
- A UI that can change in size, requiring more items to fit the view port.
- A user wanting to change the sort order.
A pivoting pipeline for the above looks like:
// Query for items describing the page and sort order
data class PageQuery(
val page: Int,
val isAscending: Boolean
)
class Loader(
isDark: Boolean,
scope: CoroutineScope
) {
// Current query that is visible in the view port
private val currentQuery = MutableStateFlow(
PageQuery(
page = 0,
isAscending = true
)
)
// Number of columns in the grid
private val numberOfColumns = MutableStateFlow(1)
// Flow specifying the pivot configuration
private val pivotRequests = combine(
currentQuery.map { it.isAscending },
numberOfColumns,
::pivotRequest
).distinctUntilChanged()
// Define inputs that match the current pivoted position
private val pivotInputs = currentQuery.toPivotedTileInputs<PageQuery, NumberTile>(
pivotRequests = pivotRequests
)
// Allows for changing the order on response to user input
private val orderInputs = currentQuery
.map { pageQuery ->
Tile.Order.PivotSorted<PageQuery, NumberTile>(
query = pageQuery,
comparator = when {
pageQuery.isAscending -> ascendingPageComparator
else -> descendingPageComparator
}
)
}
.distinctUntilChanged()
// Change limit to account for dynamic view port size
private val limitInputs = numberOfColumns.map { gridSize ->
Tile.Limiter<PageQuery, NumberTile> { items -> items.size > MIN_ITEMS_TO_SHOW * gridSize }
}
val tiledList: Flow<TiledList<PageQuery, NumberTile>> = merge(
pivotInputs,
orderInputs,
limitInputs,
)
.toTiledList(
numberTiler(
itemsPerPage = ITEMS_PER_PAGE,
isDark = isDark,
)
)
fun setCurrentPage(page: Int) = currentQuery.update { query ->
query.copy(page = page)
}
fun toggleOrder() = currentQuery.update { query ->
query.copy(isAscending = !query.isAscending)
}
fun setNumberOfColumns(numberOfColumns: Int) = this.numberOfColumns.update {
numberOfColumns
}
// Avoid breaking object equality in [PivotRequest] by using vals
private val nextQuery: PageQuery.() -> PageQuery? = {
copy(page = page + 1)
}
private val previousQuery: PageQuery.() -> PageQuery? = {
copy(page = page - 1).takeIf { it.page >= 0 }
}
/**
* Pivoted tiling with the grid size as a dynamic input parameter
*/
private fun pivotRequest(
isAscending: Boolean,
numberOfColumns: Int,
) = PivotRequest(
onCount = 4 * numberOfColumns,
offCount = 4 * numberOfColumns,
nextQuery = nextQuery,
previousQuery = previousQuery,
comparator = when {
isAscending -> ascendingPageComparator
else -> descendingPageComparator
}
)
}
private fun numberTiler(
itemsPerPage: Int,
isDark: Boolean,
): ListTiler<PageQuery, NumberTile> =
listTiler(
limiter = Tile.Limiter { items -> items.size > 40 },
order = Tile.Order.PivotSorted(
query = PageQuery(page = 0, isAscending = true),
comparator = ascendingPageComparator
),
fetcher = { pageQuery ->
pageQuery.colorShiftingTiles(itemsPerPage, isDark)
}
)
fun PageQuery.colorShiftingTiles(itemsPerPage: Int, isDark: Boolean): Flow<List<NumberTile>> {
...
}
In the above, only flows for 4 * numOfColumns queries are collected at any one time. 4 * numOfColumns more queries are kept in memory for quick
resumption, and the rest are evicted from memory. As the user scrolls, setCurrentPage
is called, and data is
fetched for that page, and the surrounding pages.
Pages that are far away from the defined range are removed from memory.