Jetpack Paging with Room Database

Android Jetpack’s Paging library combined with Room Persistence Library provides an efficient way to load and display large datasets from a local database. Paging helps you load data in chunks, improving app performance and reducing memory usage. Room provides a persistence layer, simplifying database interactions. This blog post will guide you through implementing Jetpack Paging with a Room database.

What is Jetpack Paging?

The Paging library allows you to load and display small chunks of data at a time. This approach reduces network usage and speeds up the app, as it doesn’t load the entire dataset upfront. It supports loading data from various sources, including local databases, network resources, and more.

What is Room Persistence Library?

Room is a persistence library that provides an abstraction layer over SQLite. It makes it easier to interact with SQLite databases, offering compile-time verification of SQL queries and simplifying database operations. Room integrates seamlessly with other Jetpack libraries like LiveData and Paging.

Why Use Paging with Room?

  • Efficient Data Loading: Load data in chunks, reducing memory footprint and improving app performance.
  • Simplified Database Interactions: Room simplifies database operations, providing a cleaner and more maintainable codebase.
  • Lifecycle Awareness: Jetpack components are lifecycle-aware, reducing the risk of memory leaks and ensuring efficient resource usage.
  • Better User Experience: Users experience faster loading times and a smoother scrolling experience with large datasets.

How to Implement Jetpack Paging with Room Database

To implement Paging with Room, follow these steps:

Step 1: Add Dependencies

Add the necessary dependencies to your build.gradle file:

dependencies {
    implementation "androidx.room:room-runtime:2.6.1"
    kapt "androidx.room:room-compiler:2.6.1"
    implementation "androidx.paging:paging-runtime-ktx:3.3.0-alpha03"
    implementation "androidx.room:room-paging:2.6.1"

    // Lifecycle components (optional, but recommended)
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
    kapt "androidx.lifecycle:lifecycle-compiler:2.2.0"
}

Make sure to apply the kotlin-kapt plugin in your build.gradle:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

Step 2: Define the Entity

Create a data class that represents a database entity. Annotate it with @Entity to define it as a Room entity.

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 3: Create the DAO (Data Access Object)

Create a DAO interface to define database interactions. Include a method to retrieve a PagingSource, which Paging uses to load data.

import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface ItemDao {
    @Query("SELECT * FROM items")
    fun getAllItems(): PagingSource<Int, Item>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(items: List<Item>)
}

Step 4: Define the Room Database

Create an abstract class extending RoomDatabase. Annotate it with @Database and define the entities and version.

import androidx.room.Database
import androidx.room.RoomDatabase

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

Step 5: Build the Room Database

Instantiate the Room database using Room.databaseBuilder.

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

object DatabaseProvider {
    private var instance: AppDatabase? = null

    fun getDatabase(context: Context): AppDatabase {
        if (instance == null) {
            synchronized(AppDatabase::class) {
                instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                ).build()
            }
        }
        return instance!!
    }
}

Step 6: Create a Pager in ViewModel

In your ViewModel, use Pager to create a Flow<PagingData<Item>>. This will handle loading data from the database using the PagingSource defined in the DAO.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn

class ItemViewModel(appDatabase: AppDatabase) : ViewModel() {

    val itemDao = appDatabase.itemDao()

    val items = Pager(
        PagingConfig(pageSize = 20) // You can adjust the pageSize
    ) {
        itemDao.getAllItems()
    }.flow.cachedIn(viewModelScope)
}

Note:The above example requires dependency injection to pass AppDatabase instance, it could be done using Hilt or Koin, you need to set up one to work with Viewmodel properly.

Step 7: Display Data in UI (Compose Example)

Collect the PagingData in your UI layer (e.g., in a Compose function) and display the items using a LazyColumn.


import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import androidx.compose.foundation.lazy.items

@Composable
fun ItemList(items: Flow<PagingData<Item>>) {
    val lazyPagingItems = items.collectAsLazyPagingItems()

    LazyColumn {
         items(
            count = lazyPagingItems.itemCount,
            key = { index -> lazyPagingItems[index]?.id ?: index }
        ) { index ->
            val item = lazyPagingItems[index]
            if (item != null) {
                ItemCard(item = item)
            } else {
                // Handle null item (e.g., display a placeholder or error message)
                Text("Loading...")
            }
        }
    }
}

@Composable
fun ItemCard(item: Item) {
    Card(modifier = Modifier.padding(8.dp)) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "Name: ${item.name}")
            Text(text = "Description: ${item.description}")
        }
    }
}


@Preview(showBackground = true)
@Composable
fun PreviewItemList() {
    // Note: This is a dummy preview and won't display actual data
    // You would need to provide a real or mocked Flow> for a proper preview
    Text("Paging data will be displayed here when the app is running.")
}

To integrate ItemList, call the ItemList composable and pass in the flow like so:


setContent {
            ItemList(items = viewModel.items)
        }

Handling Edge Cases

Implementing paging involves considering edge cases to ensure a smooth user experience:

  • Error Handling: Implement error handling for data loading. Show appropriate error messages in the UI when data fails to load.
  • Empty States: Display an empty state message when the dataset is empty.
  • Loading Indicators: Show loading indicators while fetching data.

Conclusion

Jetpack Paging combined with Room Database is a powerful way to efficiently load and display large datasets in Android applications. By loading data in chunks, it improves app performance and reduces memory usage. This approach is particularly useful for apps that display long lists of items from a local database, providing a smoother and more responsive user experience.