Overview
The pagination feature enables infinite scrolling in the article list, automatically loading more articles as the user scrolls to the bottom. It uses offset-based pagination managed through Room database for state persistence.
Key Components
PaginationManager : Manages pagination state and offset tracking
Room Database : Persists current pagination offset
RecyclerView ScrollListener : Detects when user reaches bottom
Loading Indicator : Visual feedback during load more
The PaginationManager handles pagination state and communicates with the local database.
Class Structure
class PaginationManager @Inject constructor ( private val paginationDao: PaginationDao )
Getting Current Offset
PaginationManager.kt:17-19
suspend fun getCurrentOffset (): Int ? {
return paginationDao. getPagination ()?.offset
}
The offset represents the starting position for the next batch of articles to load. It’s retrieved from the database to maintain state across app sessions.
PaginationManager.kt:24-37
suspend fun updatePagination (paginationDto: PaginationDto ) {
val offset = extractOffset (paginationDto.next)
paginationDao. deletePagination ()
if (paginationDto.articles. isNotEmpty ()) {
paginationDao. insertPagination (
PaginationEntity (
count = paginationDto.count,
offset = offset
)
)
}
}
Extract Offset
Parse the next page URL to get the offset parameter
Clear Old Data
Delete previous pagination state
Save New State
Insert new pagination entity if articles were received
PaginationManager.kt:42-44
private fun extractOffset (url: String ?): Int ? {
return url?. let { Uri. parse (it). getQueryParameter ( "offset" )?. toIntOrNull () }
}
The API returns a next URL containing the offset for the next page. The manager extracts this value using Android’s Uri parser.
Database Schema
Pagination state is persisted in Room database.
SpaceFlightNewsDb.kt:16-19
@Database (entities = [PaginationEntity:: class ], version = 1 )
abstract class SpaceFlightNewsDb : RoomDatabase () {
abstract fun paginationDao (): PaginationDao
}
The app detects when the user reaches the bottom of the list using a custom extension.
RecyclerView Extension
RecyclerViewExtensions.kt:18-28
fun RecyclerView . onLoadMoreScrollListener (loadMore: () -> Unit) {
addOnScrollListener ( object : RecyclerView . OnScrollListener () {
override fun onScrolled (recyclerView: RecyclerView , dx: Int , dy: Int ) {
super . onScrolled (recyclerView, dx, dy)
if ( ! recyclerView. canScrollVertically ( 1 )) {
loadMore. invoke ()
}
}
})
}
canScrollVertically(1) returns false when the RecyclerView cannot scroll down anymore, indicating the user has reached the bottom.
Fragment Setup
ArticlesFragment.kt:84-88
binding.rcvArticles. init ( this@ArticlesFragment .adapter, requireContext ())
binding.rcvArticles. onLoadMoreScrollListener {
loadMoreArticle ()
}
Load More Implementation
When the user scrolls to the bottom, the fragment triggers loading more articles.
Fragment Load More
ArticlesFragment.kt:91-95
private fun loadMoreArticle () {
if (adapter.itemCount == 0 ) return
adapter. setLoading ( true )
viewModel. fetchArticles (loadMore = true )
}
The method checks if the adapter has items before loading more to prevent loading on empty lists.
ViewModel Load More
ArticlesViewModel.kt:55-65
fun fetchArticles (query: String ? = null , reload: Boolean = false , loadMore: Boolean = false ) {
if ( ! reload && ! loadMore && _currentList. isNotEmpty ()) return
if (reload) {
_articles. value = Resource. Loading ()
}
viewModelScope. launch {
handleResult (articleUseCase. getArticles (query ?: onGetSearchQueryChanged ()), loadMore)
}
}
Handling Load More Results
ArticlesViewModel.kt:72-86
private fun handleResult (result: Resource < List < Article >>, loadMore: Boolean ) {
when (result) {
is Resource.Success -> {
if ( ! loadMore) {
_currentList. clear ()
}
_currentList. addAll (result. data ?: listOf ())
_articles. value = Resource. Success (_currentList. toList ())
}
is Resource.Error -> _articles. value = result
is Resource.Loading -> _articles. value = Resource. Loading ()
}
}
Load More vs Reload:
Load more : Appends new articles to existing list
Reload : Clears list and loads from beginning
Loading Indicator
The adapter displays a loading indicator at the bottom during pagination.
Adapter Loading State
private val viewTypeItem = 0
private val viewTypeLoading = 1
private var isLoading = false
override fun getItemViewType (position: Int ): Int {
return if (position < super . getItemCount ()) viewTypeItem else viewTypeLoading
}
override fun getItemCount (): Int {
return super . getItemCount () + if (isLoading) 1 else 0
}
Setting Loading State
fun setLoading (isLoading: Boolean ) {
if ( this .isLoading == isLoading) return
this .isLoading = isLoading
if (isLoading) {
notifyItemInserted ( super . getItemCount ())
} else {
notifyItemRemoved ( super . getItemCount ())
}
}
Repository Integration
The repository retrieves the current offset and includes it in API calls.
ArticleRepositoryImpl.kt:39-58
override suspend fun getArticles (
query: String ?
): Resource < List < Article >> {
return withContext (Dispatchers.IO) {
val response: ApiResponse < PaginationDto > =
apiHelper. safeApiCall {
api. getArticles (query, paginationManager. getCurrentOffset ())
}
when (response) {
is ApiSuccessResponse -> {
val pagination = response.body
paginationManager. updatePagination (pagination)
Resource. Success (pagination.articles. map { it. toArticleDomain () })
}
is ApiErrorResponse -> Resource. Error (response.code, response.msg, response.error)
}
}
}
User Scrolls
User scrolls to bottom of RecyclerView
Trigger Load More
ScrollListener detects bottom and calls loadMoreArticle()
Show Loading
Adapter adds loading indicator at bottom
Get Offset
PaginationManager retrieves current offset from database
API Call
Repository makes API call with offset parameter
Update Pagination
PaginationManager saves new offset from response
Append Articles
ViewModel appends new articles to existing list
Hide Loading
Adapter removes loading indicator
Pagination works seamlessly with the search feature.
ArticlesViewModel.kt:40-47
private fun observeSearchQuery () {
viewModelScope. launch {
_searchQuery. debounce ( 300 ). distinctUntilChanged (). collectLatest { query ->
_currentList. clear ()
fetchArticles (query. ifEmpty { null }, reload = true )
}
}
}
When the search query changes, the list is cleared and pagination starts from the beginning with the new query.
Benefits
Infinite Scrolling Seamless content loading without pagination buttons
State Persistence Pagination offset saved in database across sessions
Performance Loads articles in batches instead of all at once
Visual Feedback Loading indicator shows when fetching more articles
Article List Main list that implements pagination
Search Pagination works with search results
Offline Mode Pagination state persisted for offline access