package com.example.myapp.data.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.example.myapp.data.remote.ApiService
import com.example.myapp.models.Post
class PostPagingSource(
private val apiService: ApiService,
private val query: String? = null
) : PagingSource<Int, Post>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Post> {
return try {
val page = params.key ?: 1
val perPage = params.loadSize
val response = if (query != null) {
apiService.searchPosts(query, page, perPage)
} else {
apiService.getPosts(page, perPage)
}
val posts = response.posts
val nextPage = if (posts.isEmpty() || response.meta.page >= response.meta.totalPages) {
null
} else {
page + 1
}
LoadResult.Page(
data = posts,
prevKey = if (page == 1) null else page - 1,
nextKey = nextPage
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Post>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
package com.example.myapp.data.paging
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.example.myapp.data.local.AppDatabase
import com.example.myapp.data.local.PostEntity
import com.example.myapp.data.local.RemoteKeys
import com.example.myapp.data.remote.ApiService
@OptIn(ExperimentalPagingApi::class)
class PostRemoteMediator(
private val apiService: ApiService,
private val database: AppDatabase
) : RemoteMediator<Int, PostEntity>() {
private val postDao = database.postDao()
private val remoteKeysDao = database.remoteKeysDao()
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, PostEntity>
): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND -> return MediatorResult.Success(
endOfPaginationReached = true
)
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
remoteKeys?.nextKey ?: return MediatorResult.Success(
endOfPaginationReached = remoteKeys != null
)
}
}
val response = apiService.getPosts(page, state.config.pageSize)
val posts = response.posts
val endOfPaginationReached = posts.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
postDao.deleteAll()
remoteKeysDao.deleteAll()
}
val prevKey = if (page == 1) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = posts.map {
RemoteKeys(
postId = it.id,
prevKey = prevKey,
nextKey = nextKey
)
}
remoteKeysDao.insertAll(keys)
postDao.insertAll(posts.map { it.toEntity() })
}
MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
private suspend fun getRemoteKeyForLastItem(
state: PagingState<Int, PostEntity>
): RemoteKeys? {
return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { post ->
remoteKeysDao.getRemoteKeys(post.id)
}
}
}
package com.example.myapp.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.*
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@HiltViewModel
class PagingViewModel @Inject constructor(
private val apiService: ApiService,
private val database: AppDatabase
) : ViewModel() {
val postsFlow: Flow<PagingData<Post>> = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
prefetchDistance = 5,
initialLoadSize = 40
),
remoteMediator = PostRemoteMediator(apiService, database),
pagingSourceFactory = { database.postDao().observePostsPaging() }
).flow.cachedIn(viewModelScope)
fun searchPosts(query: String): Flow<PagingData<Post>> = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { PostPagingSource(apiService, query) }
).flow.cachedIn(viewModelScope)
}
// In Composable:
@Composable
fun PostsList(viewModel: PagingViewModel = hiltViewModel()) {
val posts = viewModel.postsFlow.collectAsLazyPagingItems()
LazyColumn {
items(posts.itemCount) { index ->
posts[index]?.let { post ->
PostCard(post = post)
}
}
posts.apply {
when {
loadState.refresh is LoadState.Loading -> {
item { LoadingItem() }
}
loadState.append is LoadState.Loading -> {
item { LoadingItem() }
}
loadState.refresh is LoadState.Error -> {
item { ErrorItem { posts.retry() } }
}
loadState.append is LoadState.Error -> {
item { ErrorItem { posts.retry() } }
}
}
}
}
}
Paging 3 loads large datasets incrementally with network and database support. I create a PagingSource implementing load() method to fetch pages. RemoteMediator orchestrates network and database, fetching from API and caching locally. Pager configuration sets page size and load strategies. The library returns Flow<PagingData> observed in UI. LoadState tracks loading, refreshing, and error states. cachedIn(viewModelScope) shares data across collectors. Separators inject headers with insertSeparators(). Retry mechanisms handle failed loads. Compose's LazyPagingItems or RecyclerView's PagingDataAdapter render paginated data. Paging eliminates manual page tracking and provides smooth infinite scrolling.