Building a Pagination System in XML with Room Database

Pagination is a crucial feature for applications dealing with large datasets. It allows users to navigate data in manageable chunks, improving performance and user experience. Implementing pagination efficiently involves backend data retrieval and UI representation. In this article, we’ll explore how to build a pagination system in an Android app using XML layouts and the Room database for local data persistence.

What is Pagination?

Pagination is the process of dividing a large dataset into discrete pages to improve performance and user experience. Instead of loading an entire dataset at once, the application retrieves and displays data in smaller, more manageable portions.

Why Implement Pagination?

  • Performance Improvement: Reduces initial loading time and data usage.
  • Better User Experience: Allows users to browse data more efficiently.
  • Resource Optimization: Prevents the application from overwhelming system resources.

Prerequisites

  • Android Studio installed
  • Basic knowledge of Kotlin or Java
  • Understanding of Room database and its components (Entity, DAO, Database)
  • XML layout familiarity

Step 1: Set Up a New Android Project

Create a new Android project in Android Studio, choosing Kotlin or Java as the programming language.

Step 2: Add Dependencies

Add the necessary dependencies to your build.gradle (Module: app) file.


dependencies {
    implementation \"androidx.core:core-ktx:1.9.0\"
    implementation \"androidx.appcompat:appcompat:1.6.1\"
    implementation \"com.google.android.material:material:1.9.0\"
    implementation \"androidx.constraintlayout:constraintlayout:2.1.4\"

    // Room Database
    implementation \"androidx.room:room-runtime:2.5.2\"
    kapt \"androidx.room:room-compiler:2.5.2\"
    
    // Kotlin annotation processing tool
    kapt "androidx.annotation:annotation:1.6.0"
    kapt "androidx.core:core-ktx:1.9.0"

    // Coroutines
    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3\"
    implementation \"androidx.room:room-ktx:2.5.2\" // Coroutines support for Room

    // Lifecycle Components
    implementation \"androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2\"
    implementation \"androidx.lifecycle:lifecycle-livedata-ktx:2.6.2\"
    implementation \"androidx.lifecycle:lifecycle-runtime-ktx:2.6.2\"

    testImplementation \"junit:junit:4.13.2\"
    androidTestImplementation \"androidx.test.ext:junit:1.1.5\"
    androidTestImplementation \"androidx.test.espresso:espresso-core:3.5.1\"
}

// Kotlin annotation processing
kapt {
    correctErrorTypes true
}

Remember to sync the Gradle file after adding the dependencies.

Step 3: Define the Data Model (Entity)

Create a data class that represents your data. This will be the Entity for your Room database.


import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = \"items\")
data class Item(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val name: String,
    val description: String
)

Step 4: Create the Data Access Object (DAO)

Create a DAO interface to interact with the database.


import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Dao
interface ItemDao {
    @Insert
    suspend fun insert(item: Item)

    @Query(\"SELECT * FROM items LIMIT :pageSize OFFSET (:page - 1) * :pageSize\")
    fun getItemsPaged(page: Int, pageSize: Int): Flow>

    @Query(\"SELECT COUNT(*) FROM items\")
    fun getItemCount(): Flow
}

Step 5: Define the Room Database

Create the Room database class.


import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun itemDao(): ItemDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    \"app_database\"
                )
                    .fallbackToDestructiveMigration()
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

Step 6: Create the XML Layout

Design the XML layout for your activity. This will include a RecyclerView to display the items and navigation buttons.


<?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_constraintTop_toTopOf=\"parent\"
        app:layout_constraintStart_toStartOf=\"parent\"
        app:layout_constraintEnd_toEndOf=\"parent\"
        app:layout_constraintBottom_toTopOf=\"@+id/navigationLayout\" />

    <LinearLayout
        android:id=\"@+id/navigationLayout\"
        android:layout_width=\"0dp\"
        android:layout_height=\"wrap_content\"
        android:orientation=\"horizontal\"
        android:gravity=\"center\"
        android:padding=\"8dp\"
        app:layout_constraintBottom_toBottomOf=\"parent\"
        app:layout_constraintStart_toStartOf=\"parent\"
        app:layout_constraintEnd_toEndOf=\"parent\">

        <Button
            android:id=\"@+id/prevButton\"
            android:layout_width=\"wrap_content\"
            android:layout_height=\"wrap_content\"
            android:text=\"Previous\"
            android:enabled=\"false\"
            android:layout_marginEnd=\"8dp\"/>

        <TextView
            android:id=\"@+id/pageInfoTextView\"
            android:layout_width=\"wrap_content\"
            android:layout_height=\"wrap_content\"
            android:text=\"Page 1 of 1\"/>

        <Button
            android:id=\"@+id/nextButton\"
            android:layout_width=\"wrap_content\"
            android:layout_height=\"wrap_content\"
            android:text=\"Next\"
            android:layout_marginStart=\"8dp\"/>

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

Step 7: Implement the RecyclerView Adapter

Create an adapter to populate the RecyclerView with data.


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

class ItemAdapter(private var items: List) : RecyclerView.Adapter() {

    class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val nameTextView: TextView = itemView.findViewById(R.id.nameTextView)
        val descriptionTextView: TextView = itemView.findViewById(R.id.descriptionTextView)
    }

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

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val item = items[position]
        holder.nameTextView.text = item.name
        holder.descriptionTextView.text = item.description
    }

    override fun getItemCount(): Int = items.size

    fun setItems(newItems: List) {
        items = newItems
        notifyDataSetChanged()
    }
}

Ensure you also create the item_layout.xml file for individual items:


<?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/nameTextView\"
        android:layout_width=\"match_parent\"
        android:layout_height=\"wrap_content\"
        android:textSize=\"18sp\"
        android:textStyle=\"bold\"/>

    <TextView
        android:id=\"@+id/descriptionTextView\"
        android:layout_width=\"match_parent\"
        android:layout_height=\"wrap_content\"
        android:textSize=\"14sp\"/>

</LinearLayout>

Step 8: Implement the Activity Logic

In your main activity, implement the logic to interact with the database and update the UI.


import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: ItemAdapter
    private lateinit var prevButton: Button
    private lateinit var nextButton: Button
    private lateinit var pageInfoTextView: TextView

    private var currentPage = 1
    private val pageSize = 10
    private var totalItems = 0

    private lateinit var database: AppDatabase
    private lateinit var itemDao: ItemDao

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

        // Initialize UI components
        recyclerView = findViewById(R.id.recyclerView)
        prevButton = findViewById(R.id.prevButton)
        nextButton = findViewById(R.id.nextButton)
        pageInfoTextView = findViewById(R.id.pageInfoTextView)

        // Initialize RecyclerView
        adapter = ItemAdapter(emptyList())
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter

        // Initialize Room Database
        database = AppDatabase.getDatabase(this)
        itemDao = database.itemDao()

        // Load initial data
        loadItems()

        // Set click listeners
        prevButton.setOnClickListener {
            if (currentPage > 1) {
                currentPage--
                loadItems()
            }
        }

        nextButton.setOnClickListener {
            val totalPages = (totalItems + pageSize - 1) / pageSize
            if (currentPage < totalPages) {
                currentPage++
                loadItems()
            }
        }
    }

    private fun loadItems() {
        lifecycleScope.launch {
            withContext(Dispatchers.IO) {
                itemDao.getItemCount().collectLatest { count ->
                    totalItems = count
                    updatePageInfo()
                }

                itemDao.getItemsPaged(currentPage, pageSize).collectLatest { items ->
                    withContext(Dispatchers.Main) {
                        adapter.setItems(items)
                        updateButtonState()
                    }
                }
            }
        }
    }

    private fun updatePageInfo() {
        val totalPages = (totalItems + pageSize - 1) / pageSize
        pageInfoTextView.text = \"Page \$currentPage of \$totalPages\"
    }

    private fun updateButtonState() {
        val totalPages = (totalItems + pageSize - 1) / pageSize
        prevButton.isEnabled = currentPage > 1
        nextButton.isEnabled = currentPage < totalPages
    }

    // Add initial data (for demonstration purposes)
    override fun onResume() {
        super.onResume()
        lifecycleScope.launch(Dispatchers.IO) {
            if (itemDao.getItemCount().first() == 0) {
                (1..50).forEach {
                    itemDao.insert(Item(name = \"Item \$it\", description = \"Description for item \$it\"))
                }
            }
        }
    }
}

Conclusion

By implementing pagination with the Room database in an Android application, you significantly improve the performance and user experience. This example demonstrates how to set up a Room database, create a paginated query, and display the data in a RecyclerView with navigation controls. Pagination ensures your application remains responsive and efficient, regardless of the size of your dataset. This comprehensive guide provides a robust foundation for creating scalable and user-friendly Android applications using modern architectural components and XML layouts for defining user interfaces.