To implement a 'load more on scroll' pattern, use a PagingState<T> holder to manage accumulated items, the next cursor, and loading flags. This holder should be a plain @Stable class that stores only data. To avoid memory leaks, do not pass a repository into the PagingState constructor; instead, pass the fetching function as a parameter to the loadNext() method. This ensures the holder remains lightweight and doesn't capture large objects like repositories or Context in retained state.
Key properties:
items: The list of accumulated items.isLoadingMore: An observable boolean for showing loading indicators.endReached: A boolean indicating if no more pages are available.
Use Mutex.withLock inside loadNext() to prevent duplicate requests if multiple load events are triggered simultaneously.
@Stable
class PagingState<T> {
private val loaded = mutableStateListOf<T>()
val items: List<T> get() = loaded
var isLoadingMore by mutableStateOf(false)
private set
var endReached by mutableStateOf(false)
private set
private var nextCursor: String? = null
private val mutex = Mutex()
// Pass the fetcher per call so the retained holder only stores paging data.
suspend fun loadNext(fetchPage: suspend (cursor: String?) -> Page<T>) {
// Return early when another load is already running.
if (endReached || isLoadingMore) return
mutex.withLock {
if (endReached) return
isLoadingMore = true
try {
val page = fetchPage(nextCursor)
loaded.addAll(page.items)
nextCursor = page.nextCursor
endReached = page.nextCursor == null
} finally {
isLoadingMore = false
}
}
}
}