Skip to main content

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

PaginationManager

The PaginationManager handles pagination state and communicates with the local database.

Class Structure

PaginationManager.kt:13
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.

Updating Pagination

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
            )
        )
    }
}
1

Extract Offset

Parse the next page URL to get the offset parameter
2

Clear Old Data

Delete previous pagination state
3

Save New State

Insert new pagination entity if articles were received

Extracting Offset from URL

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
}

Scroll Detection

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

ArticleAdapter.kt:14-24
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

ArticleAdapter.kt:54-63
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)
        }
    }
}

Pagination Flow

1

User Scrolls

User scrolls to bottom of RecyclerView
2

Trigger Load More

ScrollListener detects bottom and calls loadMoreArticle()
3

Show Loading

Adapter adds loading indicator at bottom
4

Get Offset

PaginationManager retrieves current offset from database
5

API Call

Repository makes API call with offset parameter
6

Update Pagination

PaginationManager saves new offset from response
7

Append Articles

ViewModel appends new articles to existing list
8

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

Build docs developers (and LLMs) love