Pagination
Updated: Dec 5, 2025
This guide explains how to work with paginated results from Horizon Platform SDK
APIs. Many APIs return large amounts of data such as leaderboard entries,
achievement definitions, and invitable users. Our platform provides a simple,
consistent pagination pattern to efficiently load and navigate through these
results.
Paginated APIs in the Platform SDK return a
PagedResults<T>
object. You primarily interact with these methods:
initialPage() - Fetch the first pagenextPage() - Navigate to the next pagepreviousPage() - Navigate to the previous pagehasNextPage() - Check if a next page existshasPreviousPage() - Check if a previous page exists.pages - A Flow that emits a
Page<T>
whenever a new page is fetched
page.contents - The list of items for the current page (Kotlin
property for getContents())page.index - The page index for ordering (Kotlin property for
getIndex(), not a zero-based array index)
The SDK handles cursor management and network requests automatically. You simply
navigate pages and observe the results.
Here’s the basic pattern for fetching paginated data:
import horizon.core.android.common.pagination.ext.*
import kotlinx.coroutines.launch
// Get paginated results from any SDK API
val pagedResults = leaderboards.getEntries(
coroutineScope = viewModelScope,
leaderboardName = "your_leaderboard",
limit = 100,
filter = LeaderboardFilterType.None,
startAt = LeaderboardStartAt.Top
)
// Fetch the initial page
viewModelScope.launch {
pagedResults.initialPage()
}
// Navigate pages
if (pagedResults.hasNextPage()) {
viewModelScope.launch { pagedResults.nextPage() }
}
if (pagedResults.hasPreviousPage()) {
viewModelScope.launch { pagedResults.previousPage() }
}
The .pages Flow is the recommended way to observe pagination updates. It emits
when you navigate to a new page:
import horizon.core.android.common.pagination.ext.pages
viewModelScope.launch {
pagedResults.pages.collect { page ->
// Get the list of entries from this page
val entries: List<LeaderboardEntry> = page.contents
// Access individual entries
entries.forEach { entry ->
println("Rank ${entry.rank}: ${entry.user.id} - Score: ${entry.score}")
}
// Or handle however you wish
}
}
What you get from each page:
page.contents - List of items (e.g., List<LeaderboardEntry>)page.index - Current page index for ordering
The Flow emits after initialPage(), nextPage(), and previousPage() calls.
A streamlined ViewModel implementation for paginated leaderboards:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import horizon.core.android.common.pagination.PageFetchException
import horizon.core.android.common.pagination.PagedResults
import horizon.core.android.common.pagination.ext.*
import horizon.platform.leaderboards.models.LeaderboardEntry
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
data class LeaderboardsUiState(
val entries: List<LeaderboardEntry> = emptyList(),
val hasNext: Boolean = false,
val hasPrevious: Boolean = false,
val errorMessage: String? = null
)
class LeaderboardsPaginationViewModel : ViewModel() {
private var pagedResults: PagedResults<LeaderboardEntry>? = null
private val _uiState = MutableStateFlow(LeaderboardsUiState())
val uiState: StateFlow<LeaderboardsUiState> = _uiState
init {
// Observe page updates
viewModelScope.launch {
pagedResults?.pages?.collect { page ->
_uiState.value = LeaderboardsUiState(
entries = page.contents,
hasNext = pagedResults?.hasNextPage() ?: false,
hasPrevious = pagedResults?.hasPreviousPage() ?: false
)
}
}
}
fun loadLeaderboard(name: String) {
tryFetch {
pagedResults = leaderboards.getEntries(
coroutineScope = viewModelScope,
leaderboardName = name,
limit = 100,
filter = LeaderboardFilterType.None,
startAt = LeaderboardStartAt.Top
)
pagedResults?.initialPage()
}
}
fun loadNextPage() {
tryFetch { pagedResults?.nextPage() }
}
fun loadPreviousPage() {
tryFetch { pagedResults?.previousPage() }
}
private fun tryFetch(block: suspend () -> Unit) {
viewModelScope.launch {
try {
block()
} catch (e: PageFetchException) {
_uiState.value = _uiState.value.copy(
errorMessage = e.message ?: "Unknown error"
)
}
}
}
}
Jetpack Compose integration
Use collectAsStateWithLifecycle() to observe the pagination state in Compose:
@Composable
fun LeaderboardScreen(viewModel: LeaderboardsPaginationViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Column {
// Show errors
uiState.errorMessage?.let { Text(text = it) }
// Navigation
Row {
Button(
onClick = { viewModel.loadPreviousPage() },
enabled = uiState.hasPrevious
) {
Text("Previous")
}
Button(
onClick = { viewModel.loadNextPage() },
enabled = uiState.hasNext
) {
Text("Next")
}
}
// Display entries
LazyColumn {
items(uiState.entries) { entry ->
Text("${entry.rank}. ${entry.user.id} - ${entry.score}")
}
}
}
}
Handle PageFetchException when navigating pages:
try {
pagedResults.nextPage()
} catch (e: PageFetchException) {
// Handle error
}
- Always call
initialPage() before accessing results or using the .pages
Flow - Check pagination state with
hasNextPage() and hasPreviousPage() before
navigating - Use the
.pages Flow for reactive UIs instead of manually accessing
getFetchedPages() - Handle errors by catching
PageFetchException on all navigation
operations - Choose reasonable page sizes (50-100 items) to balance performance and
memory usage
- Let the SDK manage cursors - you don’t need to track or pass them manually