Jetpack Paging 3 is a powerful library used to load and display large datasets efficiently in Android apps using Kotlin. However, developers often encounter issues where Paging 3 “does not work” as expected such as data not loading, RecyclerView not updating, or endless loading indicators. This article will guide you through common issues and their fixes when using Paging 3 with Kotlin, complete with sample code and practical solutions.
What is Paging 3 in Android?
Paging 3 is a library from Android Jetpack that helps load data from a large dataset in chunks or “pages”. It supports Kotlin coroutines, Flow, and integrates well with Room, Retrofit, and ViewModel.

Basic architecture:
- PagingSource: defines how to load data.
- Pager: builds Flow of PagingData.
- PagingDataAdapter: submits and displays the paged data.
Common Issues When Paging 3 is Not Working
Let’s go through the most common scenarios where Paging 3 doesn’t behave as expected, and how to resolve them.
1. PagingDataAdapter Shows Blank List
Problem:
Data is not showing in your RecyclerView
even though the setup seems correct.
Fix:
Make sure you are collecting PagingData
inside a lifecycleScope
.
viewModel.pagingDataFlow .onEach { pagingData -> adapter.submitData(pagingData) } .launchIn(lifecycleScope)
Ensure this is done inside onViewCreated()
or onCreate()
, not inside onCreateView()
.
2. PagingSource Not Called
Problem:
Your PagingSource.load()
method is not triggered at all.
Fix:
- Confirm
Pager
is properly initialized and the flow is collected. - Make sure you’re using the correct scope and not missing
.collectLatest
.
val flow = Pager( config = PagingConfig(pageSize = 20), pagingSourceFactory = { MyPagingSource() } ).flow val pagingDataFlow = flow.cachedIn(viewModelScope)
Also, confirm you’re not calling submitData()
before collecting pagingDataFlow
.
3. Stuck on Loading State Forever
Problem:
The loading indicator shows endlessly, and data never appears.
Fix:
Check the implementation of PagingSource.load()
for returning the correct LoadResult
.
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MyData> { return try { val page = params.key ?: 1 val response = apiService.getData(page) LoadResult.Page( data = response.items, prevKey = if (page == 1) null else page - 1, nextKey = if (response.items.isEmpty()) null else page + 1 ) } catch (e: Exception) { LoadResult.Error(e) } }
Avoid returning null data or failing silently. Always wrap with try-catch.
4. No Data After Refresh or Retry
Problem:
Refreshing data doesn’t reload or it keeps loading the same data.
Fix:
Make sure PagingSource
is invalidated when a data source is changed.
fun refresh() { currentPagingSource?.invalidate() }
Or use .cachedIn(viewModelScope)
properly to ensure the data updates correctly.
5. Item Comparison Not Working
Problem:
UI doesn’t update after data changes.
Fix:
Ensure your DiffUtil.ItemCallback
is properly implemented in your adapter.
object MyDataComparator : DiffUtil.ItemCallback<MyData>() { override fun areItemsTheSame(oldItem: MyData, newItem: MyData): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: MyData, newItem: MyData): Boolean { return oldItem == newItem } }
Complete Sample Setup
1. ViewModel.kt
class MainViewModel : ViewModel() { val pagingDataFlow = Pager( config = PagingConfig(pageSize = 20), pagingSourceFactory = { MyPagingSource() } ).flow.cachedIn(viewModelScope) }
2. Fragment.kt
class MainFragment : Fragment() { private val viewModel by viewModels<MainViewModel>() private lateinit var adapter: MyPagingAdapter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { adapter = MyPagingAdapter() binding.recyclerView.adapter = adapter.withLoadStateHeaderAndFooter( header = MyLoadStateAdapter { adapter.retry() }, footer = MyLoadStateAdapter { adapter.retry() } ) lifecycleScope.launch { viewModel.pagingDataFlow.collectLatest { adapter.submitData(it) } } } }
3. PagingSource.kt
class MyPagingSource : PagingSource<Int, MyData>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MyData> { val page = params.key ?: 1 return try { val response = ApiService.getData(page) LoadResult.Page( data = response.items, prevKey = if (page == 1) null else page - 1, nextKey = if (response.items.isEmpty()) null else page + 1 ) } catch (e: Exception) { LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<Int, MyData>): Int? { return state.anchorPosition?.let { position -> state.closestPageToPosition(position)?.prevKey?.plus(1) ?: state.closestPageToPosition(position)?.nextKey?.minus(1) } } }
Testing Paging 3
To debug Paging issues:
- Use
Logcat
insideload()
to track which page is being loaded. - Implement
LoadStateListener
to track loading, error, and empty states:
adapter.addLoadStateListener { loadState -> val isListEmpty = loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0 // Show empty state or loading accordingly }
Conclusion
When Paging 3 does not work as expected in Kotlin-based projects, it’s often due to lifecycle scope issues, incorrect Flow collection, or problems in the PagingSource. By following these common fixes and examples, you can quickly diagnose and solve problems to ensure your app loads paged data efficiently.