How to Implement Paging 3 with XML in Android

Android Jetpack’s Paging 3 library offers an efficient way to load and display large datasets from local or network data sources. While it’s commonly used with Jetpack Compose, implementing Paging 3 with XML-based layouts is also possible and still relevant for projects that haven’t migrated to Compose. This post guides you through integrating Paging 3 with XML in an Android application.

What is Paging 3?

Paging 3 is an Android Jetpack library that helps you load and display large datasets from local or network sources in a performant and user-friendly manner. It supports asynchronous operations, handles lifecycle events, and simplifies error handling.

Why Use Paging 3 with XML?

  • Efficient Data Loading: Loads data in chunks, reducing memory usage.
  • Improved Performance: Asynchronously fetches data without blocking the UI thread.
  • Lifecycle Awareness: Integrates with Android lifecycles to avoid memory leaks.
  • Backwards Compatibility: Suitable for existing projects using XML layouts.

How to Implement Paging 3 with XML in Android

To implement Paging 3 with XML, follow these steps:

Step 1: Add Dependencies

Include the necessary dependencies in your build.gradle file:

dependencies {
    implementation "androidx.paging:paging-runtime:3.2.1" // Or the latest version
    implementation "androidx.recyclerview:recyclerview:1.3.2" // Required for RecyclerView
}

Step 2: Define Data Source

Create a PagingSource that fetches data from your data source (e.g., a network API or database). This class defines how to retrieve data pages and provides refresh capabilities.

import androidx.paging.PagingSource
import androidx.paging.PagingState

class MyPagingSource : PagingSource<Int, MyItem>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MyItem> {
        val pageNumber = params.key ?: 0
        val pageSize = params.loadSize

        return try {
            val response = ApiService.getData(pageNumber, pageSize) // Replace with your API call
            val items = response.data

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

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

// Sample Data Class
data class MyItem(val id: Int, val name: String)

// Sample API Service (replace with your actual API call)
object ApiService {
    suspend fun getData(page: Int, pageSize: Int): ApiResponse {
        // Simulate network delay
        delay(1000)
        val items = (page * pageSize until (page + 1) * pageSize).map {
            MyItem(it, "Item $it")
        }
        return ApiResponse(items)
    }
}

data class ApiResponse(val data: List<MyItem>)

Key points:

  • PagingSource: Inherit from this class and specify the key type (Int in this case) and the data type (MyItem).
  • load(): This suspending function fetches a page of data based on the provided LoadParams. It returns a LoadResult which can be either Page or Error.
  • getRefreshKey(): This function helps PagingSource to fetch the nearest available page on refresh.

Step 3: Create a PagedAdapter

Create a PagedDataAdapter to display the paged data in a RecyclerView. This adapter handles the differences between pages of data.

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView

class MyPagedAdapter : PagingDataAdapter<MyItem, MyPagedAdapter.MyViewHolder>(DIFF_UTIL) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
        return MyViewHolder(view)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = getItem(position)
        item?.let {
            holder.bind(it)
        }
    }

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val textView: TextView = itemView.findViewById(R.id.itemTextView)

        fun bind(item: MyItem) {
            textView.text = item.name
        }
    }

    companion object {
        private val DIFF_UTIL = object : DiffUtil.ItemCallback<MyItem>() {
            override fun areItemsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
                return oldItem == newItem
            }
        }
    }
}

Key aspects:

  • Inherit from PagingDataAdapter, specifying the data type (MyItem) and the ViewHolder type (MyViewHolder).
  • Implement onCreateViewHolder() to inflate your XML layout.
  • Implement onBindViewHolder() to bind data to your views.
  • Use DiffUtil for efficient updates, specifying how to check for item differences.

Step 4: Define the XML Layout

Create the layout XML file for each item in the RecyclerView. This example uses a simple TextView.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/itemTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="16sp"/>

</LinearLayout>

Step 5: Set Up RecyclerView in Your Activity

In your Activity or Fragment, initialize the RecyclerView and the PagedAdapter.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: MyPagedAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        recyclerView = findViewById(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)
        adapter = MyPagedAdapter()
        recyclerView.adapter = adapter

        val pager = Pager(
            PagingConfig(pageSize = 20)
        ) {
            MyPagingSource()
        }

        lifecycleScope.launch {
            pager.flow.collectLatest { pagingData ->
                adapter.submitData(pagingData)
            }
        }
    }
}

Key steps:

  • Initialize RecyclerView and your PagedAdapter.
  • Create a Pager instance with a PagingConfig specifying the page size.
  • Collect the Flow of PagingData and submit it to the adapter.

Step 6: Activity Layout

Add the RecyclerView to your main layout XML:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Advanced Considerations

  • Error Handling: Implement error handling in your PagingSource and display error states in your UI.
  • Loading State: Use a LoadStateAdapter to show loading indicators and retry buttons.
  • Network Requests: Utilize libraries like Retrofit or Ktor for making network requests within your PagingSource.

Conclusion

Implementing Paging 3 with XML in Android allows you to efficiently load and display large datasets, improving performance and user experience. While newer projects often use Jetpack Compose, integrating Paging 3 with XML remains a valuable skill for maintaining and updating existing applications. This comprehensive guide provides all the steps needed to set up and utilize Paging 3 in your XML-based Android projects.