Blog Infos
Author
Published
Topics
,
Author
Published

 

Table of Contents
Prerequisites

We’ll use Retrofit & Hilt in this article, so it’s better you know how they work.

Also, we’ll use this API for testing. I recommend you register and get your API key.

Getting Started
def paging_version = "3.1.1"
implementation "androidx.paging:paging-runtime:$paging_version"
implementation "androidx.paging:paging-compose:1.0.0-alpha17"

//Other Dependencies
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

def hilt_version = "2.44"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"

Don’t forget to add Internet permission in AndroidManifest.xml,

<uses-permission android:name="android.permission.INTERNET" />
Setting up Retrofit

Before we setup Retrofit, let’s see the response of the endpoint that we’ll use. Endpoint, https://newsapi.org/v2/everything?q=apple&sortBy=popularity&apiKey=APIKEY&pageSize=20&page=1

{
  "status": "ok",
  "totalResults": 65739,
  "articles": [
    {
      "source": {
        "id": "wired",
        "name": "Wired"
      },
      "author": "Parker Hall",
      "title": "Apple Music Sing Adds 'Karaoke Mode' to Streaming Songs",
      "description": "America's most popular music streaming service is adding the ability to turn down the vocals and sing along.",
      "url": "https://www.wired.com/story/apple-music-sing/",
      "urlToImage": "https://media.wired.com/photos/638f959b54aee410695ffa12/191:100/w_1280,c_limit/Apple-Music-Sing-Featured-Gear.jpg",
      "publishedAt": "2022-12-06T20:51:11Z",
      "content": "When it comes to advanced technical features and seamless compatibility with iOS devices, Apple Music has Spotify well and truly beaten. The Swedish streaming giant has essentially the same content l… [+3348 chars]"
    },
  ]
}

Response models,

Please put them into different files. I’ve put them into one code block to make it easier to read.

data class NewsResponse(
    val articles: List<Article>,
    val status: String,
    val totalResults: Int
)
data class Source(
    val id: String,
    val name: String
)
data class Article(
    val author: String,
    val content: String,
    val description: String,
    val publishedAt: String,
    val source: Source,
    val title: String,
    val url: String,
    val urlToImage: String
)

Now let’s create API Service & repository, it’s going to be a simple one,

interface NewsApiService {
    @GET("everything?q=apple&sortBy=popularity&apiKey=${Constants.API_KEY}&pageSize=20")
    suspend fun getNews(
        @Query("page") page: Int
    ): NewsResponse
}

That’s it. Now we can start implementing pagination.

Pagination with Paging 3
Paging Source

Let’s start by creating Paging Source,

class NewsPagingSource(
    private val newsApiService: NewsApiService,
): PagingSource<Int, Article>() {
    override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        return try {
            val page = params.key ?: 1
            val response = newsApiService.getNews(page = page)

            LoadResult.Page(
                data = response.articles,
                prevKey = if (page == 1) null else page.minus(1),
                nextKey = if (response.articles.isEmpty()) null else page.plus(1),
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}

The primary Paging library component in the repository layer is PagingSource. Each PagingSource object defines a source of data and how to retrieve data from that source. A PagingSource object can load data from any single source, including network sources and local databases.

In our example, PagingSource extends <Int, Article>,

Int is the type of paging key, for our case it’s index numbers for pages.

Article is the type of data loaded.

getRefreshKey, provides a key used for the initial load for the next PagingSource due to invalidation of this PagingSource.

load, function will be called by the Paging library to asynchronously fetch more data to be displayed as the user scrolls around.

That’s it for PagingSource, we can create repository & view model.

Repository & View Model

It’s not really necessary to have repository since PagingSource acts like one, you can remove repository and make the same function calls in view model.

class NewsRepository @Inject constructor(
     private val newsApiService: NewsApiService
) {
    fun getNews() = Pager(
        config = PagingConfig(
            pageSize = 20,
        ),
        pagingSourceFactory = {
            NewsPagingSource(newsApiService)
        }
    ).flow
}
@HiltViewModel
class NewsViewModel @Inject constructor(
    private val repository: NewsRepository,
): ViewModel() {

    fun getBreakingNews(): Flow<PagingData<Article>> = repository.getNews().cachedIn(viewModelScope)
}

The Pager component provides a public API for constructing instances of PagingData that are exposed in reactive streams, based on a PagingSource object and a PagingConfig configuration object.

PagingConfig, this class sets options regarding how to load content from a PagingSource such as how far ahead to load, the size request for the initial load, and others. The only mandatory parameter you have to define is the page size

pagingSourceFactory, function that defines how to create the PagingSource.

That’s it. Now we can implement UI and see the results.

UI Layer
@Composable
fun PagingListScreen() {
val viewModel = hiltViewModel<NewsViewModel>()
val articles = viewModel.getBreakingNews().collectAsLazyPagingItems()
LazyColumn {
items(
items = articles,
key = { it.url }
) { article ->
Text(
modifier = Modifier
.height(75.dp),
text = article?.title ?: "",
)
Divider()
}
when (val state = articles.loadState.refresh) { //FIRST LOAD
is LoadState.Error -> {
//TODO Error Item
//state.error to get error message
}
is LoadState.Loading -> { // Loading UI
item {
Column(
modifier = Modifier
.fillParentMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier
.padding(8.dp),
text = "Refresh Loading"
)
CircularProgressIndicator(color = Color.Black)
}
}
}
else -> {}
}
when (val state = articles.loadState.append) { // Pagination
is LoadState.Error -> {
//TODO Pagination Error Item
//state.error to get error message
}
is LoadState.Loading -> { // Pagination Loading UI
item {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(text = "Pagination Loading")
CircularProgressIndicator(color = Color.Black)
}
}
}
else -> {}
}
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

collectAsLazyPagingItems, collects values from this Flow of PagingData and represents them inside a LazyPagingItems instance. The LazyPagingItems instance can be used by the items and itemsIndexed methods from LazyListScope in order to display the data obtained from a Flow of PagingData.

First, we create LazyColumn and inside of it we use items which expects LazyPagingItems<T> and set a unique value for key. That’s it. We don’t have to do anything, as we fetch & paginate data will be inserted into LazyColumn.

Since we also need to indicate when our data is being fetched, we’ll need to show loading UI to users. LazyPagingItems comes for the rescue. LazyPagingItems<T> has loadState object which is CombinedLoadStates.

CombinedLoadStates.source is a LoadStates type, with fields for three different types of LoadState:

  • LoadStates.append: For the LoadState of items being fetched after the user’s current position.
  • LoadStates.prepend: For the LoadState of items being fetched before the user’s current position.
  • LoadStates.refresh: For the LoadState of the initial load.

Each LoadState itself can be one of the following:

  • LoadState.Loading: Items are being loaded.
  • LoadState.NotLoading: Items are not being loaded.
  • LoadState.Error: There was a loading error.

For the initial load, we check articles.loadState.refresh and if state is LoadState.Loading we show loading UI.

For the pagination, we check articles.loadState.append and if state is LoadState.Loading again and show loading UI.

You can find the full code at the end of the article.

That’s it. Let’s see the result.

Paging 3 Pagination

Pagination without Paging 3

Before we start, you might ask why do we reinvent the wheel? Because in some cases Paging 3 can cause boilerplate code and increase the complexity. Implementing pagination without Paging 3 can give us more freedom and less boilerplate code.

Since we’ve already implemented ApiService, we can start by creating repository.

Repository
class NewsManuelPagingRepository @Inject constructor(
    private val newsApiService: NewsApiService
) {
    suspend fun getNews(page: Int): Flow<NewsResponse> = flow {
       try {
           emit(newsApiService.getNews(page))
       } catch (error: Exception) {
           emit(NewsResponse(emptyList(), error.message ?: "", 0))
       }
    }.flowOn(Dispatchers.IO)
}

This is very simple and poorly executed for our example, and I do not recommend you use it this way in production. You can check these articles for more information,

View Model

Before we create view model, we’ll create enum class for List State.

enum class ListState {
    IDLE,
    LOADING,
    PAGINATING,
    ERROR,
    PAGINATION_EXHAUST,
}

This enum class will help us for managing state. Now we can create view model.

@HiltViewModel
class NewsManuelPagingViewModel @Inject constructor(
private val repository: NewsManuelPagingRepository,
): ViewModel() {
val newsList = mutableStateListOf<Article>()
private var page by mutableStateOf(1)
var canPaginate by mutableStateOf(false)
var listState by mutableStateOf(ListState.IDLE)
init {
getNews()
}
fun getNews() = viewModelScope.launch {
if (page == 1 || (page != 1 && canPaginate) && listState == ListState.IDLE) {
listState = if (page == 1) ListState.LOADING else ListState.PAGINATING
repository.getNews(page).collect() {
if (it.status == "ok") {
canPaginate = it.articles.size == 20
if (page == 1) {
newsList.clear()
newsList.addAll(it.articles)
} else {
newsList.addAll(it.articles)
}
listState = ListState.IDLE
if (canPaginate)
page++
} else {
listState = if (page == 1) ListState.ERROR else ListState.PAGINATION_EXHAUST
}
}
}
}
override fun onCleared() {
page = 1
listState = ListState.IDLE
canPaginate = false
super.onCleared()
}
}

First, we have 3 variables,

page is for keeping the page number. canPaginate is to check if we can paginate further or if there is any error. listState is the state variable for the UI.

Inside of init we make the first request, we are fetching first page when view model object created.

getNews function’s logic can be change depending on the endpoints and requirements. In this example, we set listState to Loading or Paginating depending on the page number and make the endpoint call.

Since endpoint returns status: "ok" for successful request, we check if it is successful or not. If it is successful, we insert new items to the list and set the values for canPaginate and listState.

That’s it. Logic is very simple and open to improvements. You can test it yourself and change it accordingly.

Finally, let’s see the UI.

UI Layer

This is going to be a little longer, so we’ll go part by part.

val viewModel = hiltViewModel<NewsManuelPagingViewModel>()
val lazyColumnListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()

val shouldStartPaginate = remember {
    derivedStateOf {
        viewModel.canPaginate && (lazyColumnListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -9) >= (lazyColumnListState.layoutInfo.totalItemsCount - 6)
    }
}

val articles = viewModel.newsList

lazyColumnListState is necessary to get the visible item info for Lazy Column.

shouldStartPaginate is to determine whether or not we should start paginating. We’ll use derivedStateOffor better performance. You can read more from this link.

First, we check if we can paginate or not, viewModel.canPaginate,

Then, we get the last visible item’s index, lazyColumnListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index, and check if the index number is bigger than or equal to total number of item count, lazyColumnListState.layoutInfo.totalItemsCount, minus some number that you decide. I decided to set it 6 for our case. You can change it depending on your list and size.

LaunchedEffect(key1 = shouldStartPaginate.value) {
    if (shouldStartPaginate.value && viewModel.listState == ListState.IDLE)
        viewModel.getNews()
}

We’ll use LaunchedEffect to start pagination and make request. Whenever shouldStartPaginate.value changes, we start the pagination and that’s it.

Now, we can create Lazy Column,

LazyColumn(state = lazyColumnListState) {
items(
items = articles,
key = { it.url },
) { article ->
Text(
modifier = Modifier
.height(75.dp),
text = article.title,
)
Divider()
}
item (
key = viewModel.listState,
) {
when(viewModel.listState) {
ListState.LOADING -> {
Loading()
}
ListState.PAGINATING -> {
PaginationLoading()
}
ListState.PAGINATION_EXHAUST -> {
PaginationExhaust()
}
else -> {}
}
}
}

Setting state = lazyColumnListState is very important to listen pagination, don’t forget it!

I think only part that requires a little bit of an explanation is when(viewModel.listState) and it’s very simple. With the help of enum class that we’ve created earlier, we check the state of the list and show necessary UI.

You can find the full code at the end of the article.

That’s it. Let’s see the results.

Pagination without Paging 3

You can contact me on,

This article was originally published on proandroiddev.com on December 17, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu