📄 Paginacja

KNET oferuje helper do obsługi paginacji: offset, cursor i Link header.

Offset pagination

import rip.nerd.kitsunenet.pagination.KNETPagination

val pagination = KNETPagination.offset(
    pageSize = 20,
    startPage = 1
)

// Pierwsza strona
val page1 = client.get("$baseUrl/users", query = pagination.params)
// ?page=1&limit=20

// Następna strona
pagination.nextPage()
val page2 = client.get("$baseUrl/users", query = pagination.params)
// ?page=2&limit=20

// Iteruj wszystkie strony
pagination.forEachPage { params ->
    val response = client.get("$baseUrl/users", query = params)
    val users = response.jsonList<User>()

    if (users.isEmpty()) {
        return@forEachPage false // Stop
    }

    processUsers(users)
    true // Continue
}

Cursor pagination

val pagination = KNETPagination.cursor(
    limitParam = "limit",
    cursorParam = "cursor",
    pageSize = 20
)

// Pierwsza strona
val response1 = client.get("$baseUrl/users", query = pagination.params)
// ?limit=20

// Pobierz cursor z response
val nextCursor = response1.jsonPath("meta.next_cursor") as? String
pagination.setCursor(nextCursor)

// Następna strona
val response2 = client.get("$baseUrl/users", query = pagination.params)
// ?limit=20&cursor=abc123

// Auto-paginacja
pagination.fetchAll(client, "$baseUrl/users") { response ->
    val data = response.jsonObject()
    val cursor = data["meta"]?.let { (it as Map<*,*>)["next_cursor"] } as? String
    val items = (data["data"] as List<*>).map { User.fromMap(it as Map<*,*>) }

    PaginationResult(items, cursor)
}

Link header pagination

// Automatyczne parsowanie Link header (RFC 5988)
val pagination = KNETPagination.linkHeader()

val response = client.get("$baseUrl/users?page=1")
// Link: </users?page=2>; rel="next", </users?page=10>; rel="last"

val links = pagination.parseLinks(response)
println(links.next)  // "/users?page=2"
println(links.last)  // "/users?page=10"
println(links.hasNext)  // true

// Fetch next
if (links.hasNext) {
    val nextResponse = client.get("$baseUrl${links.next}")
}

Flow-based pagination

fun getUsers(): Flow<User> = flow {
    val pagination = KNETPagination.offset(pageSize = 20)

    while (true) {
        val response = client.get("$baseUrl/users", query = pagination.params)
        val users = response.jsonList<User>()

        if (users.isEmpty()) break

        users.forEach { emit(it) }

        pagination.nextPage()
    }
}

// Użycie
getUsers()
    .take(100)  // Max 100 users
    .collect { user ->
        displayUser(user)
    }

Paging 3 integration

class UserPagingSource(
    private val client: KNETClient
) : PagingSource<Int, User>() {

    override suspend fun load(
        params: LoadParams<Int>
    ): LoadResult<Int, User> {
        val page = params.key ?: 1

        return try {
            val response = client.get("$baseUrl/users", query = mapOf(
                "page" to page.toString(),
                "limit" to params.loadSize.toString()
            ))

            val users = response.jsonList<User>()

            LoadResult.Page(
                data = users,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (users.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, User>): Int? {
        return state.anchorPosition?.let { pos ->
            state.closestPageToPosition(pos)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(pos)?.nextKey?.minus(1)
        }
    }
}

// W ViewModel
val usersPager = Pager(PagingConfig(pageSize = 20)) {
    UserPagingSource(client)
}.flow.cachedIn(viewModelScope)

Infinite scroll helper

class InfiniteScrollHelper<T>(
    private val pageSize: Int = 20,
    private val loadPage: suspend (page: Int) -> List<T>
) {
    private var currentPage = 1
    private var isLoading = false
    private var hasMore = true

    private val _items = MutableStateFlow<List<T>>(emptyList())
    val items: StateFlow<List<T>> = _items

    private val _state = MutableStateFlow<LoadState>(LoadState.Idle)
    val state: StateFlow<LoadState> = _state

    suspend fun loadInitial() {
        currentPage = 1
        _items.value = emptyList()
        loadMore()
    }

    suspend fun loadMore() {
        if (isLoading || !hasMore) return

        isLoading = true
        _state.value = LoadState.Loading

        try {
            val newItems = loadPage(currentPage)

            hasMore = newItems.size >= pageSize
            _items.update { it + newItems }
            currentPage++
            _state.value = LoadState.Idle

        } catch (e: Exception) {
            _state.value = LoadState.Error(e.message ?: "Error")
        } finally {
            isLoading = false
        }
    }
}

sealed class LoadState {
    object Idle : LoadState()
    object Loading : LoadState()
    data class Error(val message: String) : LoadState()
}

📚 Zobacz też