Skip to main content

Using Room Database

1. Setting Up Room Dependencies

Room is a SQLite object mapping library that provides a robust framework for database operations in Android applications. To use Room, you must include the necessary dependencies in your project.

Dependency Version Description
androidx.room:room-runtime 2.5.0 Main Room library for database operations
androidx.room:room-ktx 2.5.0 Kotlin coroutines support for Room
androidx.room:room-testing 2.5.0 Testing utilities for Room databases

Example: Adding Room Dependencies to build.gradle (Module: app)

dependencies {
    implementation "androidx.room:room-runtime:2.5.0"
    implementation "androidx.room:room-ktx:2.5.0"
    kapt "androidx.room:room-compiler:2.5.0"
}
Note: The kapt plugin is required for Room's annotation processor to generate the necessary database classes at compile time.

2. Creating Entity Classes

Property Description Example
@Entity Annotation to mark a class as a Room entity @Entity(tableName = "users")
tableName Optional property to specify the table name in the database @Entity(tableName = "users")
primaryKeys Defines the primary key(s) of the entity @Entity(primaryKeys = ["id"])
indices Defines indices for faster querying @Entity(indices = [Index(value = ["name"])])
foreignKeys Defines foreign key constraints @Entity(foreignKeys = [ForeignKey(...)])

Example: Basic Entity Class

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    
    @ColumnInfo(name = "full_name")
    val name: String,
    
    @ColumnInfo(name = "email_address")
    val email: String
)

Example: Entity with Composite Primary Key

@Entity(
    tableName = "user_preferences",
    primaryKeys = ["userId", "preferenceKey"]
)
data class UserPreference(
    val userId: Int,
    val preferenceKey: String,
    
    @ColumnInfo(name = "preference_value")
    val value: String
)
Note: The @PrimaryKey annotation can be used with autoGenerate = true to automatically generate unique IDs.
Warning: All properties in an entity class must be public or have public getters and setters. Room uses reflection to access these properties.

3. Defining Data Access Objects

Data Access Objects (DAOs) in Room define the methods for accessing data from the database. They are interfaces or abstract classes annotated with @Dao.

Method Description Return Type Notes
insert() Inserts one or more entities into the database void Can be overloaded for multiple entities
update() Updates one or more entities in the database void Must have matching primary keys
delete() Deletes one or more entities from the database void Can delete by entity or by ID
getAll() Retrieves all entities from the table List<Entity> Typically returns a LiveData or Flow
findById() Retrieves an entity by its primary key Entity? Returns null if not found

Example: Basic DAO Definition

@Dao
interface UserDao {
    @Insert
    suspend fun insertUser(user: User)

    @Update
    suspend fun updateUser(user: User)

    @Delete
    suspend fun deleteUser(user: User)

    @Query("SELECT * FROM users")
    suspend fun getAllUsers(): List<User>

    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUserById(userId: Int): User?
}

Example: Using LiveData with DAO

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): LiveData<List<User>>

    @Query("SELECT * FROM users WHERE name = :name")
    fun getUsersByName(name: String): LiveData<List<User>>
}
Note: DAO methods can be suspend functions if using Room's coroutines support or return LiveData for automatic UI updates.
Warning: Avoid using Flow directly in DAOs as Room does not support it natively. Use LiveData or Suspend functions.

4. Building Room Database

Example: Basic Room Database Setup

@Database(
    entities = [User::class],
    version = 1
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}
Component Description Notes
@Database Annotation to mark the database class Must be applied to an abstract class extending RoomDatabase
entities List of entity classes in the database Each entity must be annotated with @Entity
version Database version for migration handling Increment when schema changes
abstract class Must extend RoomDatabase Room generates implementation at compile time

Example: Using Room Database in Application

class MyApplication : Application() {
    val database by lazy {
        Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "app_database"
        ).build()
    }
}
Note: The database name should be unique to avoid conflicts in multi-module apps.
Warning: Room database creation is expensive; use lazy initialization or singleton pattern.

Key Points

  • Use @Database annotation to define the database class
  • Specify entities and version number
  • Implement abstract DAOs for data access
  • Use Room.databaseBuilder() to create database instance

5. Using Type Converters

Type converters are used to tell Room how to convert custom types to and from types that Room can store in the database.

Converter Input Type Output Type Description
@TypeConverter Date Long Converts Date to milliseconds since epoch
@TypeConverter List<String> String Converts list of strings to comma-separated string
@TypeConverter Uri String Converts Uri to its string representation

Example: Converting Date to Long

@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
    return date?.time
}

@TypeConverter
fun timestampToDate(timestamp: Long?): Date? {
    return timestamp?.let { Date(it) }
}

Example: Converting List to String

@TypeConverter
fun listToString(list: List<String&gt?): String? {
    return list?.joinToString(",")
}

@TypeConverter
fun stringToList(string: String?): List<String>? {
    return string?.split(",")
}
Note: Type converters must be declared in a separate class annotated with @TypeConverters and applied to the database class.

6. Implementing Relationships

Room supports defining relationships between entities using foreign keys and queries. These relationships can be one-to-one, one-to-many, or many-to-many.

Relationship Type Description Implementation
One-to-One Each record in one table is associated with exactly one record in another table. Use a foreign key in the child entity that references the primary key of the parent.
One-to-Many One record in a parent table can be associated with multiple records in a child table. Define a foreign key in the child entity referencing the parent's primary key.
Many-to-Many Multiple records in one table can be associated with multiple records in another table. Use a junction table to define the relationship between two entities.

Example: One-to-Many Relationship

import androidx.room.*

@Entity(tableName = "users")
data class User(
    @PrimaryKey val userId: Int,
    val name: String
)

@Entity(tableName = "posts", foreignKeys = [
    ForeignKey(
        entity = User::class,
        parentColumns = ["userId"],
        childColumns = ["authorId"],
        onDelete = ForeignKey.CASCADE
    )
])
data class Post(
    @PrimaryKey val postId: Int,
    val title: String,
    val content: String,
    val authorId: Int
)

Example: Many-to-Many Relationship

import androidx.room.*

// Junction table for many-to-many relationship
@Entity(
    primaryKeys = ["userId", "postId"],
    foreignKeys = [
        ForeignKey(
            entity = User::class,
            parentColumns = ["userId"],
            childColumns = ["userId"]
        ),
        ForeignKey(
            entity = Post::class,
            parentColumns = ["postId"],
            childColumns = ["postId"]
        )
    ]
)
data class UserPost(
    val userId: Int,
    val postId: Int
)

// User entity
@Entity(tableName = "users")
data class User(
    @PrimaryKey val userId: Int,
    val name: String
)

// Post entity
@Entity(tableName = "posts")
data class Post(
    @PrimaryKey val postId: Int,
    val title: String,
    val content: String
)
Note: When defining relationships, ensure that the foreign key constraints are properly configured to maintain referential integrity.

7. Observing Data Changes

Room provides built-in support for observing data changes using LiveData and Flow. These observables allow your UI to automatically update when the underlying database data changes.

Observable Type Description Usage Example
LiveData Part of Android Architecture Components, observes data changes and updates UI on main thread. fun getAllUsers(): LiveData<List<User>>
Flow Kotlin coroutine-based observable, more flexible and composable than LiveData. suspend fun getAllUsers(): Flow<List<User>>

Example: Using LiveData to observe data changes

class UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): LiveData<List<User>>
}

class UserViewModel(private val userDao: UserDao) : ViewModel() {
    val allUsers: LiveData<List<User>> = userDao.getAllUsers()
}

Example: Using Flow to observe data changes

class UserDao {
    @Query("SELECT * FROM users")
    suspend fun getAllUsers(): Flow<List<User>>
}

class UserViewModel(private val userDao: UserDao) : ViewModel() {
    fun getAllUsers(): Flow<List<User>> = userDao.getAllUsers()
}
Note: When using Flow, make sure to collect it in a coroutine scope to avoid memory leaks.

Key Points for Observing Data Changes

  • LiveData: Best for Android Architecture Components integration, automatically observes changes on the main thread.
  • Flow: More flexible, supports backpressure and cancellation, ideal for modern Kotlin apps.
  • Use Cases: Observing lists, single items, or filtered results from Room.

8. Performing Migrations

When you update your app's database schema, you must handle migrations to ensure existing data is preserved. Room provides a RoomDatabase.Callback mechanism to manage schema changes.

Migration Type Description Usage
Auto Migrations Room can automatically handle simple schema changes (e.g., adding columns) without requiring a custom migration. @Database( ... , autoMigrations = [AutoMigration(from = 1, to = 2)])
Custom Migration For complex schema changes, define a custom migration using Migration class. val MIGRATION_1_2 = object : Migration(1, 2) { ... }

Example: Custom Migration

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE users ADD COLUMN last_login TEXT")
    }
}
Note: Always test migrations on a copy of production data to ensure no data loss or corruption.

Migration Best Practices

  • Use autoMigrations for simple schema additions.
  • Define custom migrations for complex changes like renaming or deleting columns.
  • Ensure backward compatibility when updating the database version.

9. Using Database Transactions

Room provides support for database transactions to ensure data consistency and atomicity. Transactions are essential when performing multiple operations that must either all succeed or all fail.

Transaction Type Description Usage
Transaction Ensures all operations within the transaction are executed as a single unit. val db = Room.databaseBuilder(context, MyDatabase::class.java, "mydb").build()

Example: Using Room Transaction

val db = Room.databaseBuilder(context, MyDatabase::class.java, "mydb").build()

db.runInTransaction {
    // Insert data
    val user = User(1, "John Doe")
    userDao.insert(user)

    // Update data
    val updatedUser = User(1, "John Smith")
    userDao.update(updatedUser)

    // Delete data
    userDao.delete(user)
}
Note: The runInTransaction method ensures that all operations within the lambda are executed atomically. If any operation fails, the entire transaction is rolled back.
Warning: Avoid long-running transactions as they can block other operations and lead to performance issues.

Key Points

  • Use runInTransaction to group multiple database operations.
  • All operations within a transaction are atomic.
  • Transaction failures result in a rollback of all changes.

10. Testing Room Databases

Example: Unit Test with In-Memory Database

import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class RoomDatabaseTest {

    private lateinit var db: MyDatabase
    private lateinit var userDao: UserDao

    @Before
    fun createDb() {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        db = Room.inMemoryDatabaseBuilder(context, MyDatabase::class.java).build()
        userDao = db.userDao()
    }

    @After
    fun closeDb() {
        db.close()
    }

    @Test
    fun insertAndRetrieveUser() {
        val user = User(1, "John Doe")
        userDao.insert(user)

        val retrievedUser = userDao.getUserById(1)
        assert(retrievedUser?.name == "John Doe")
    }
}
Feature Description Notes
Room In-Memory Database Room.inMemoryDatabaseBuilder() Useful for unit tests to avoid file I/O
Test Database Room.databaseBuilder() For integration tests with persistent storage
Mocking DAOs Use Mockito or mock implementations Allows testing without actual DB interaction
Note: Use Room.inMemoryDatabaseBuilder for fast, isolated unit tests.
Warning: Avoid using real database files in tests to prevent data corruption.

Example: Setting up FTS with Room

import androidx.room.*

@Entity
@Fts4
data class Article(
    @PrimaryKey
    val id: Int,
    val title: String,
    val content: String
)

@Dao
interface ArticleDao {
    @Query("SELECT * FROM articles WHERE articles MATCH :query")
    fun searchArticles(query: String): List<Article>
}
Feature Description Notes
@Fts4 Full-Text Search table annotation Available in Room 2.4+
MATCH SQL operator for FTS queries Used in @Query methods
tokenize Tokenization options for FTS Can use porter, unicode61
Note: FTS queries use a special syntax that differs from standard SQL. The MATCH operator is required for full-text matching.
Warning: FTS tables do not support foreign key constraints or triggers.

Key Points

  • Room supports full-text search via @Fts4
  • Use MATCH operator in queries for search terms
  • FTS tables are optimized for text search, not general data storage

12. Using Room with Paging

Example: Setting up PagedList with Room

import androidx.paging.PagedList
import androidx.room.*

@Entity
data class User(
    @PrimaryKey val id: Int,
    val name: String,
    val email: String
)

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAllUsers(): LiveData<PagedList<User>>
}

@Database(
    entities = [User::class],
    version = 1
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}
Component Description Usage
PagedList Immutable list of data that can be paginated Used in DAO queries to return paged results
PagingSource Abstract class to load data in pages Override to define how data is loaded from database
PagedListAdapter Adapter for displaying paged data in RecyclerView Extends RecyclerView.Adapter to handle paged lists
Note: Room with Paging 3 requires AndroidX Paging 3 library.

Example: Using PagingSource with Room

import androidx.paging.PagingSource
import androidx.room.*

class UserPagingSource(
    private val userDao: UserDao
) : PagingSource<Int, User>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
        val position = params.key ?: 0
        val pageSize = params.loadSize
        val users = userDao.getUsersPage(position, pageSize)
        return LoadResult.Page(
            data = users,
            prevKey = if (position == 0) null else position - pageSize,
            nextKey = if (users.isEmpty()) null else position + pageSize
        )
    }

    override fun getRefreshKey(state: PagingState<Int, User>): Int? {
        return state.anchorPosition?.minus(state.config.pageSize / 2)
    }
}

Example: Configuring PagedListAdapter

import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil

class UserAdapter : PagedListAdapter<User, UserViewHolder>(
    object : DiffUtil.ItemCallback<User>() {
        override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem == newItem
        }
    }
) {
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(getItem(position)!!)
    }

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

Was this article helpful?