DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at zenn.dev

Cursor-Based Pagination with Infinite Scroll in Compose

Cursor-Based Pagination with Infinite Scroll in Compose

Cursor-based pagination is efficient for large datasets. Implement infinite scroll by loading more items as the user scrolls.

Data Model

data class PaginatedResponse<T>(
    val data: List<T>,
    val nextCursor: String? = null,
    val hasMore: Boolean = false
)

data class Post(
    val id: String,
    val title: String,
    val content: String,
    val timestamp: Long
)
Enter fullscreen mode Exit fullscreen mode

Repository with Pagination

class PostRepository(private val api: PostApi) {
    suspend fun getPosts(cursor: String? = null): PaginatedResponse<Post> {
        return api.fetchPosts(cursor = cursor)
    }
}
Enter fullscreen mode Exit fullscreen mode

ViewModel State Management

class PostViewModel(private val repository: PostRepository) : ViewModel() {
    private val _posts = MutableStateFlow<List<Post>>(emptyList())
    val posts: StateFlow<List<Post>> = _posts.asStateFlow()

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    private var nextCursor: String? = null

    fun loadMore() {
        viewModelScope.launch {
            _isLoading.value = true
            val response = repository.getPosts(nextCursor)
            _posts.value += response.data
            nextCursor = response.nextCursor
            _isLoading.value = false
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Infinite Scroll Compose

@Composable
fun InfinitePostList(viewModel: PostViewModel) {
    val posts by viewModel.posts.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()
    val listState = rememberLazyListState()

    LaunchedEffect(listState) {
        snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
            .collect { lastIndex ->
                if (lastIndex != null && lastIndex >= posts.size - 3 && !isLoading) {
                    viewModel.loadMore()
                }
            }
    }

    LazyColumn(state = listState) {
        items(posts) { post ->
            PostCard(post)
        }
        if (isLoading) {
            item { LoadingIndicator() }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

API Request

interface PostApi {
    @GET("posts")
    suspend fun fetchPosts(
        @Query("cursor") cursor: String? = null,
        @Query("limit") limit: Int = 20
    ): PaginatedResponse<Post>
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • Use cursor tokens for stateless pagination
  • Trigger loading when user is 3 items from end
  • Avoid duplicate items by tracking cursors
  • Handle network errors with retry logic

8 Android app templates on Gumroad

Top comments (0)