<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zahidul Islam</title>
    <description>The latest articles on DEV Community by Zahidul Islam (@zahid_dev).</description>
    <link>https://dev.to/zahid_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3293412%2F85a47143-64c7-431d-9969-6b073b880ad2.jpg</url>
      <title>DEV Community: Zahidul Islam</title>
      <link>https://dev.to/zahid_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zahid_dev"/>
    <language>en</language>
    <item>
      <title>Mastering Android Pagination with Paging 3 + Jetpack Compose</title>
      <dc:creator>Zahidul Islam</dc:creator>
      <pubDate>Wed, 25 Jun 2025 11:32:42 +0000</pubDate>
      <link>https://dev.to/zahid_dev/mastering-android-pagination-with-paging-3-jetpack-compose-4foc</link>
      <guid>https://dev.to/zahid_dev/mastering-android-pagination-with-paging-3-jetpack-compose-4foc</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Imagine scrolling through an app that loads images or content before you even realize you need them-no loading spinners. No waiting. No lag. Just a smooth, seamless experience. Well, that's exactly what the latest Paging library in Android Jetpack, called Paging 3, brings to the table, with some cool features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paging 3 comes with a powerful Kotlin-first, coroutine-supported API&lt;/strong&gt; that makes pagination easier and more efficient. One of the key performance features of Paging 3 is its &lt;strong&gt;smart prefetching&lt;/strong&gt;. It doesn't just wait for the user to reach the end of the list - it starts loading the &lt;strong&gt;next page of data off-screen&lt;/strong&gt;, so by the time the user scrolls down, the content is already ready.&lt;/p&gt;

&lt;p&gt;This signifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No visible loading indicators between pages&lt;/li&gt;
&lt;li&gt;Smooth, uninterrupted scroll&lt;/li&gt;
&lt;li&gt;Better perceived performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, I'll walk through building an image-loading app using your sample project: &lt;a href="https://github.com/zahid-git/Paging3-Image-Loading" rel="noopener noreferrer"&gt;Paging3‑Image‑Loading&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;The app fetches images in pages from a remote API and displays them in a grid using Jetpack Compose. &lt;/p&gt;

&lt;p&gt;It highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Paging 3&lt;/strong&gt; for smooth pagination and lazy loading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose&lt;/strong&gt; with LazyColumn and LazyPagingItems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coil&lt;/strong&gt; for efficient image loading.&lt;/li&gt;
&lt;li&gt;Built-in handling of loading and error states.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup Dependencies
&lt;/h2&gt;

&lt;p&gt;To begin with, add the following Paging 3 dependencies to your &lt;code&gt;libs.versions.toml&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;implementation "androidx.paging:paging-compose:3.3.6"
implementation "androidx.paging:paging-runtime-ktx:3.3.6"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please do check if any latest version has been released from here: &lt;a href="https://developer.android.com/jetpack/androidx/releases/paging" rel="noopener noreferrer"&gt;https://developer.android.com/jetpack/androidx/releases/paging&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure PagingSource
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PagingSource&lt;/code&gt; tells the Paging library how to fetch data, whether from a database or a network. It's where we define how to load a page of data and how to determine the next and previous page keys.&lt;/p&gt;

&lt;p&gt;In our case, we're fetching images from a remote API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class ImagePagingSource(
    private val apiService: ApiService
) : PagingSource&amp;lt;Int, ImageListModel&amp;gt;() {

    private val numOfOffScreenPage: Int = 4

    override suspend fun load(params: LoadParams&amp;lt;Int&amp;gt;): LoadResult&amp;lt;Int, ImageListModel&amp;gt; {
        val pageIndex = params.key ?: 1
        val pageSize = params.loadSize
        return try {
            val responseData = apiService.fetchImages(pageIndex, pageSize)

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

    override fun getRefreshKey(state: PagingState&amp;lt;Int, ImageListModel&amp;gt;): Int? {
        return state.anchorPosition?.let { anchor -&amp;gt;
            state.closestPageToPosition(anchor)?.prevKey?.plus(numOfOffScreenPage)
                ?: state.closestPageToPosition(anchor)?.nextKey?.minus(numOfOffScreenPage)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are 2 override functions, one is load() and the other is getRefreshKey().&lt;/p&gt;

&lt;p&gt;Within the &lt;code&gt;load()&lt;/code&gt; Function: data is retrieved either from a local database or a remote API, depending on the implementation. The &lt;code&gt;load()&lt;/code&gt; method receives &lt;code&gt;LoadParams&lt;/code&gt; as a parameter, which provides access to the current page key (&lt;code&gt;key&lt;/code&gt;), the number of items to load (&lt;code&gt;loadSize&lt;/code&gt;), and the &lt;code&gt;placeholdersEnabled&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;After fetching the required data, &lt;code&gt;LoadResult&lt;/code&gt; must be returned to the Paging framework. The &lt;code&gt;LoadResult&lt;/code&gt; can be one of three types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LoadResult.Page&lt;/strong&gt; – Used when data is successfully loaded. It contains the list of items along with optional prevKey and nextKey.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoadResult.Error&lt;/strong&gt; – Used when an error occurs during data fetching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoadResult.Invalid&lt;/strong&gt; – Used when the result is invalid. This return type can be used to terminate future load requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;In the repository layer, we call the Pager object which connects to the PagingSource. It exposes a Flow&amp;gt; to the ViewModel, allowing the UI to observe paginated data. Inside the Pager, we define the page size and supply the PagingSource. The repository doesn't fetch data directly-it delegates that to the PagingSource's load method. The LoadResult return by PagingSource is automatically transformed into PagingData for the UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class ImageRepositoryImpl @Inject constructor(
    private val apiService: ApiService
) : ImageRepository, NetworkCallback() {

    override fun getImages(
        pageSize: Int,
        enablePlaceHolders: Boolean,
        prefetchDistance: Int,
        initialLoadSize: Int,
        maxCacheSize: Int
    ): Flow&amp;lt;PagingData&amp;lt;ImageListModel&amp;gt;&amp;gt; {
        return Pager(
            config = PagingConfig(
                pageSize = pageSize,
                enablePlaceholders = enablePlaceHolders,
                prefetchDistance = prefetchDistance,
                initialLoadSize = initialLoadSize,
                maxSize = maxCacheSize
            ), pagingSourceFactory = {
                ImagePagingSource(apiService)
            }
        ).flow
    }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usecase
&lt;/h2&gt;

&lt;p&gt;The Use Case acts as an intermediary between the ViewModel and the Repository. It abstracts the business logic and simply invokes the repository'sgetImages() method. The Use Case returns a Flow&amp;gt;, keeping the ViewModel decoupled from data source implementations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class ImageLoadingUseCase(
    private val imageRepository: ImageRepository
) {

    fun fetchImages(): Flow&amp;lt;PagingData&amp;lt;ImageListModel&amp;gt;&amp;gt; {
        return imageRepository.getImages(
            pageSize = 20,
            enablePlaceHolders = false,
            prefetchDistance = 10,
            initialLoadSize = 20,
            maxCacheSize = 2000
        )
    }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ViewModel
&lt;/h2&gt;

&lt;p&gt;In the ViewModel layer, we collect the paginated data flow from the Use Case and expose it to the UI. Typically, this is done using Flow&amp;gt; and collected with collectAsLazyPagingItems() In Compose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val getImageList = imageUseCase.fetchImages().cachedIn(viewModelScope)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  User Interface
&lt;/h2&gt;

&lt;p&gt;In the UI layer, &lt;code&gt;LazyColumn&lt;/code&gt; is used to display paginated items efficiently. The items() block consumes productItems, which is a &lt;code&gt;LazyPagingItems&amp;lt;T&amp;gt;&lt;/code&gt; from the Paging 3 library. Each item is accessed by index (position) and rendered inside a Card. An image is loaded asynchronously using AsyncImage, and metadata (like author name) is overlaid using a Column inside a Box. Paging handles automatic loading of more items when the user scrolls to the end. The UI reacts to paging state updates (like loading or errors) when managed properly with &lt;code&gt;collectAsLazyPagingItems()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val productItems = viewModel.getImageList.collectAsLazyPagingItems()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LazyColumn(
    modifier = Modifier
        .fillMaxSize()
) {
    items(productItems!!.itemCount) {position-&amp;gt;
        var itemValue = productItems[position]
        Card (
            modifier = Modifier
                .padding(10.dp)
                .fillMaxWidth()
                .wrapContentHeight()
                .background(Color.Transparent)
        ) {
            Box {
                itemValue?.download_url?.let {
                    AsyncImage(
                        modifier = Modifier
                            .fillMaxWidth()
                            .wrapContentHeight()
                            .aspectRatio(itemValue.width!!*1.0f / itemValue.height!!),
                        model = itemValue.download_url.toString(),
                        contentDescription = "Image",
                        contentScale = ContentScale.Fit
                    )
                }

                Column (
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.Gray)
                        .height(30.dp)
                        .align(Alignment.BottomStart)
                        .padding(start = 10.dp),
                    verticalArrangement = Arrangement.Center
                ) {
                    Text(
                        color = Color.White,
                        text = itemValue?.author.toString()
                    )
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;To wrap it up, Paging 3 paired with clean architecture isn't just a design pattern - it's a productivity boost for any list-heavy Android app. With each layer playing its part - PagingSource fetching data, the Repository serving it, the Use Case shaping it, and the ViewModel delivering it to a Jetpack Compose-powered UI-you get a clean, testable, and scalable flow. It not only keeps your codebase neat but also gives users a buttery-smooth scrolling experience, even with massive datasets.&lt;/p&gt;

&lt;p&gt;🚀 Ready to dive in or steal some code? Check out the complete working example on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/zahid-git/Paging3-Image-Loading" rel="noopener noreferrer"&gt;View Github Repository&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>jetpackcompose</category>
      <category>pagining3</category>
    </item>
  </channel>
</rss>
