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"
}
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
)
@PrimaryKey annotation can be used with autoGenerate = true to automatically generate unique IDs.
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>>
}
suspend functions if using Room's coroutines support or return LiveData for automatic UI updates.
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()
}
}
Key Points
- Use
@Databaseannotation 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>?): String? {
return list?.joinToString(",")
}
@TypeConverter
fun stringToList(string: String?): List<String>? {
return string?.split(",")
}
@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
)
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()
}
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")
}
}
Migration Best Practices
- Use
autoMigrationsfor 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)
}
runInTransaction method ensures that all operations within the lambda are executed atomically. If any operation fails, the entire transaction is rolled back.
Key Points
- Use
runInTransactionto 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 |
Room.inMemoryDatabaseBuilder for fast, isolated unit tests.11. Implementing Full-Text Search
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 |
MATCH operator is required for full-text matching.Key Points
- Room supports full-text search via
@Fts4 - Use
MATCHoperator 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 |
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)
}
}