Pagination: With remote API - devrath/ComposeAlchemy GitHub Wiki
-
API-ENDPOINT:
https://quotable.io/quotes
- API-BASE-RESPONSE: Complete Response
{
"count": 20,
"totalCount": 2127,
"page": 1,
"totalPages": 107,
"lastItemIndex": 20,
"results": [
{
"_id": "bfNpGC2NI",
"author": "Thomas Edison",
"content": "As a cure for worrying, work is better than whisky.",
"tags": [
"Humorous"
],
"authorSlug": "thomas-edison",
"length": 51,
"dateAdded": "2023-04-14",
"dateModified": "2023-04-14"
},
{
"_id": "ghVnmSpeAo",
"author": "Thomas Edison",
"content": "Everything comes to him who hustles while he waits.",
"tags": [
"Success",
"Motivational"
],
"authorSlug": "thomas-edison",
"length": 51,
"dateAdded": "2023-04-14",
"dateModified": "2023-04-14"
},
]
}
-
API-ENDPOINT:
https://quotable.io/quotes?page=2
- API-PAGE-RESPONSE: Complete Response
{
"count": 20,
"totalCount": 2127,
"page": 2,
"totalPages": 107,
"lastItemIndex": 40,
"results": [
{
"_id": "XtZMaD2s28",
"author": "Thomas Edison",
"content": "If we all did the things we are capable of doing, we would literally astound ourselves.",
"tags": [
"Inspirational",
"Motivational"
],
"authorSlug": "thomas-edison",
"length": 87,
"dateAdded": "2023-04-14",
"dateModified": "2023-04-14"
},
{
"_id": "niVz2fQWSH",
"author": "Thomas Edison",
"content": "Opportunity is missed by most people because it is dressed in overalls and looks like work.",
"tags": [
"Opportunity",
"Work"
],
"authorSlug": "thomas-edison",
"length": 91,
"dateAdded": "2023-04-14",
"dateModified": "2023-04-14"
}
]
}
PagingSource
/**
* In the paging source, We define how to load the data from the API.
* *************
* PagingSource<Int, Result>
* <*> First Param: The type of the key used to load the data.
* URL: https://quotable.io/quotes?page=2
* Here the value `2` is the key used to load the data which is integer type
* <*> Second Param: The type is the base response data which we will receive from the API.
*/
class QuotePagingSource(private val quoteAPI: QuoteAPI) : PagingSource<Int, Result>() {
/**
* OBJECTIVE: In this method we will load the data from the API.
*/
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Result> {
// Params contains the information on what page to load next
return try {
// Determine the page number: When we do not have the `key`(It will be null) in the param, So we load the first page
val position = params.key ?: 1
val response = quoteAPI.getQuotes(position)
// Load the page
return LoadResult.Page(
// Define the current data
data = response.results,
// <---> Define the previous page key <--->
// If the position = 1 ==> This indicates ths there is no previous page
// If the position > 1 ==> This indicates that there is a previous page (position - 1)
prevKey = if (position == 1) null else position - 1,
// <---> Define the next page key <--->
// If the position = totalPages ==> This indicates that there is no next page
// If the position < totalPages ==> This indicates that there is a next page (position + 1)
nextKey = if (position == response.totalPages) null else position + 1
)
} catch (e: Exception) {
// Load the error
LoadResult.Error(e)
}
}
/**
* OBJECTIVE: This function will help us telling which page to load next.
* Based on the anchor position we determine which page to load next
* ******
* Basically the anchor position maintains the current position of the page.
*/
override fun getRefreshKey(state: PagingState<Int, Result>): Int? {
return state.anchorPosition?.let { anchorPosition ->
// When we pass the anchor position the `closestPageToPosition` function it will get you the closest page to that position
val anchorPage = state.closestPageToPosition(anchorPosition)
// Once we have the anchor position, We will see the previous key or next key of the page
// Here we are checking if the current page has a previous key ==> If previous key is not there then check the next key
// If the current page has a next key ==> If next key is not there then check the previous key
// ----> If none of the key is there then return null --> In such case it will get the first page
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
Composables
@Composable
fun QuotesDemo(navController: NavHostController) {
val viewModel: QuotesDemoViewModel = hiltViewModel()
val quotesPagingItems = viewModel.list.collectAsLazyPagingItems()
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top
) {
items(quotesPagingItems.itemCount) { index ->
val quote = quotesPagingItems[index]
quote?.let {
QuoteItem(it)
}
}
}
}
}
@Composable
fun QuoteItem(quote: Result) {
Column(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier
.fillMaxWidth()
.padding(8.dp)) {
Text(
text = quote.content,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 4.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "Author: ${quote.author}",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 2.dp)
)
Text(
text = "Date Added: ${quote.dateAdded}",
style = MaterialTheme.typography.titleSmall
)
}
HorizontalDivider(
modifier = Modifier.fillMaxWidth(),
thickness = 2.dp,
color = Color.Gray
)
}
}
ViewModel
@HiltViewModel
class QuotesDemoViewModel @Inject constructor(
repository : QuoteRepository
) : ViewModel() {
val list = repository.getQuotes().cachedIn(viewModelScope)
}
Repository
class QuoteRepository @Inject constructor(private val quoteAPI: QuoteAPI) {
fun getQuotes(): Flow<PagingData<Result>> = Pager(
config = PagingConfig(pageSize = 20, maxSize = 100),
pagingSourceFactory = { QuotePagingSource(quoteAPI) }
).flow
}
API
interface QuoteAPI {
@GET("/quotes")
suspend fun getQuotes(@Query("page") page: Int): QuoteList
}
di
@OptIn(ExperimentalPagingApi::class)
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Singleton
@Provides
fun getRetrofit(): Retrofit {
return Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()).build()
}
@Singleton
@Provides
fun getQuoteAPI(retrofit: Retrofit): QuoteAPI {
return retrofit.create(QuoteAPI::class.java)
}
}
Models
data class Result(
val _id: String,
val author: String,
val authorSlug: String,
val content: String,
val dateAdded: String,
val dateModified: String,
val length: Int,
val tags: List<String>
)
data class QuoteList(
val count: Int,
val lastItemIndex: Int,
val page: Int,
val results: List<Result>,
val totalCount: Int,
val totalPages: Int
)