Develop
Develop
Select your platform

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.

Overview

Paginated APIs in the Platform SDK return a PagedResults<T> object. You primarily interact with these methods:
  • initialPage() - Fetch the first page
  • nextPage() - Navigate to the next page
  • previousPage() - Navigate to the previous page
  • hasNextPage() - Check if a next page exists
  • hasPreviousPage() - 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.

Core pagination pattern

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

Working with Flows

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.

ViewModel integration

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

Error handling

Handle PageFetchException when navigating pages:
try {
    pagedResults.nextPage()
} catch (e: PageFetchException) {
    // Handle error
}

Best practices

  • 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

API reference

See the complete API documentation at PagedResults<T>.
Did you find this page helpful?