Explore the Android Room Persistence Library for efficient database management, including setup, entities, DAOs, and integration with Kotlin Coroutines.
Room simplifies database operations in Android. It offers:
- Compile-time query checks
- Less boilerplate code
- Automatic object mapping
- Built-in migration support
Key parts:
- Entities (database tables)
- DAOs (database operations)
- Database class
Room works well with:
Feature | Room | SQLite |
---|---|---|
Query validation | Compile-time | Runtime |
Object mapping | Automatic | Manual |
Boilerplate code | Minimal | Extensive |
Migration support | Built-in | Manual |
This guide covers setup, entities, DAOs, database management, testing, and optimization for Room in Android.
Related video from YouTube
Room Architecture Basics
Room provides an abstract layer over SQLite with three main components:
-
Entity: Represents a database table
@Entity data class User( @PrimaryKey val uid: Int, @ColumnInfo(name = "first_name") val firstName: String?, @ColumnInfo(name = "last_name") val lastName: String? )
-
DAO: Defines database operations
@Dao interface UserDao { @Query("SELECT * FROM user") fun getAll(): List<User> @Insert fun insertAll(vararg users: User) @Delete fun delete(user: User) }
-
Database: Main access point for app's data
@Database(entities = [User::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }
Room simplifies database operations:
- App interacts with Database instance
- Database uses DAOs for operations
- DAOs interact with Entities (database tables)
This structure allows for more organized code compared to raw SQLite queries.
Adding Room to Your Project
To add Room to your Android project:
- Include dependencies in
build.gradle
:
dependencies {
def room_version = "2.5.2"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
}
- Apply plugins in project-level
build.gradle
:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
-
Sync project with Gradle files.
-
For latest Android and Jetpack Compose, use Kotlin Symbol Processing (KSP):
plugins {
id 'com.google.devtools.ksp' version '1.8.21-1.0.11'
}
dependencies {
def room_version = "2.5.2"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
ksp "androidx.room:room-compiler:$room_version"
}
- Sync project again after changes.
Creating Entities
Entities represent database tables in Room. Here's how to create them:
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
@ColumnInfo(name = "email_address") val email: String
)
Key annotations:
Annotation | Purpose | Example |
---|---|---|
@Entity | Marks class as entity | @Entity(tableName = "books") |
@PrimaryKey | Defines primary key | @PrimaryKey val id: Int |
@ColumnInfo | Specifies column details | @ColumnInfo(name = "author_name") val author: String |
@Ignore | Excludes field from table | @Ignore val temporaryFlag: Boolean |
For entity relationships:
- Define parent entity:
@Entity(tableName = "schools")
data class School(
@PrimaryKey val id: Int,
val name: String
)
- Define child entity with foreign key:
@Entity(
tableName = "students",
foreignKeys = [ForeignKey(
entity = School::class,
parentColumns = ["id"],
childColumns = ["school_id"],
onDelete = ForeignKey.CASCADE
)]
)
data class Student(
@PrimaryKey val id: Int,
val name: String,
@ColumnInfo(name = "school_id") val schoolId: Int
)
- Create relationship class:
data class SchoolWithStudents(
@Embedded val school: School,
@Relation(
parentColumn = "id",
entityColumn = "school_id"
)
val students: List<Student>
)
This setup allows querying a school and its students together.
Using Data Access Objects (DAOs)
DAOs define database operations in Room:
@Dao
interface UserDao {
@Insert
suspend fun insertUser(user: User)
@Query("SELECT * FROM users")
fun getAllUsers(): List<User>
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
}
Basic operations:
Operation | Annotation | Description |
---|---|---|
Create | @Insert | Adds data |
Read | @Query | Retrieves data |
Update | @Update | Modifies data |
Delete | @Delete | Removes data |
For custom queries:
@Query("SELECT * FROM users WHERE age BETWEEN :minAge AND :maxAge")
fun getUsersBetweenAges(minAge: Int, maxAge: Int): List<User>
Use with Flow for reactive programming:
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsersAlphabetically(): Flow<List<User>>
Setting Up and Managing Databases
Create your RoomDatabase class:
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
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"
).build()
INSTANCE = instance
instance
}
}
}
}
For database updates:
- Increase version number
- Create Migration class:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
- Add migration to database builder:
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2)
.build()
For non-critical data, use:
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
.fallbackToDestructiveMigration()
.build()
Advanced Room Features
Room offers advanced features for better performance:
- Type Converters:
class Converters {
@TypeConverter
fun fromBitmap(bitmap: Bitmap?): ByteArray? {
// Convert Bitmap to ByteArray
}
@TypeConverter
fun toBitmap(byteArray: ByteArray?): Bitmap? {
// Convert ByteArray to Bitmap
}
}
@Database(entities = [Run::class], version = 1)
@TypeConverters(Converters::class)
abstract class RunningDatabase : RoomDatabase() {
// Database implementation
}
- Full-text search:
@Fts4
@Entity(tableName = "notes")
data class Note(
@PrimaryKey @ColumnInfo(name = "rowid") val id: Int,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "content") val content: String
)
@Dao
interface NoteDao {
@Query("SELECT * FROM notes WHERE notes MATCH :query")
fun searchNotes(query: String): List<Note>
}
- Transactions:
@Dao
abstract class InvoiceStore {
@Insert
abstract fun insertInvoice(invoice: Invoice)
@Insert
abstract fun insertItems(items: List<InvoiceItem>)
@Transaction
open fun insertInvoiceWithItems(invoice: Invoice, items: List<InvoiceItem>) {
insertInvoice(invoice)
insertItems(items)
}
}
- Background queries:
@Dao
interface UserDao {
@Query("SELECT * FROM users")
suspend fun getAllUsers(): List<User>
}
// In ViewModel or Repository
viewModelScope.launch {
val users = userDao.getAllUsers()
// Update UI with users
}
Room with Kotlin Coroutines
Room works well with Kotlin Coroutines for efficient database operations:
- Add dependency:
dependencies {
implementation "androidx.room:room-ktx:2.5.0"
}
- Create suspend functions in DAOs:
@Dao
interface UserDao {
@Query("SELECT * FROM user")
suspend fun getAll(): List<User>
@Insert
suspend fun insertAll(users: List<User>)
@Delete
suspend fun delete(user: User)
}
- Use in ViewModel:
class UserViewModel(private val userDao: UserDao) : ViewModel() {
fun loadUsers() {
viewModelScope.launch {
val users = userDao.getAll()
// Update UI with users
}
}
}
- Use Flow for observable queries:
@Dao
interface UserDao {
@Query("SELECT * FROM user ORDER BY name ASC")
fun getAllUsersFlow(): Flow<List<User>>
}
sbb-itb-bfaad5b
Room with LiveData and Flow
Room integrates with LiveData and Flow for real-time data updates:
- LiveData in Room queries:
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsers(): LiveData<List<User>>
}
class UserViewModel(private val userDao: UserDao) : ViewModel() {
val allUsers: LiveData<List<User>> = userDao.getAllUsers()
}
// In UI
viewModel.allUsers.observe(this) { users ->
// Update UI with users
}
- Flow for data updates:
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsersFlow(): Flow<List<User>>
}
class UserViewModel(private val userDao: UserDao) : ViewModel() {
val allUsers: StateFlow<List<User>> = userDao.getAllUsersFlow()
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
}
// In UI
lifecycleScope.launch {
viewModel.allUsers.collect { users ->
// Update UI with users
}
}
Feature | LiveData | Flow |
---|---|---|
Lifecycle awareness | Yes | No (needs lifecycle-aware scope) |
Backpressure handling | No | Yes |
Operator support | Limited | Extensive |
UI thread safety | Yes | No (needs main dispatcher) |
Testing Room Code
Test Room implementation with JUnit and Mockito:
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
userDao = database.userDao()
}
@After
fun cleanup() {
database.close()
}
@Test
fun insertAndRetrieveUser() = runBlocking {
val user = User(1, "John Doe")
userDao.insert(user)
val retrievedUser = userDao.getUserById(1)
assertEquals(user, retrievedUser)
}
}
For full database tests:
@RunWith(AndroidJUnit4::class)
class AppDatabaseTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
private lateinit var postDao: PostDao
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
userDao = database.userDao()
postDao = database.postDao()
}
@Test
fun userWithPostsTest() = runBlocking {
val user = User(1, "Jane Doe")
userDao.insert(user)
val post = Post(1, 1, "Test Post")
postDao.insert(post)
val userWithPosts = userDao.getUserWithPosts(1)
assertEquals(1, userWithPosts.posts.size)
assertEquals("Test Post", userWithPosts.posts[0].content)
}
}
Use in-memory databases for faster testing:
Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries() // For testing only
.build()
Making Room Faster
Optimize Room for better performance:
- Write better queries:
- Use specific column names
- Add LIMIT to queries
- Use WHERE clauses
@Query("SELECT id, name FROM users WHERE active = 1 LIMIT 100")
fun getActiveUsers(): List<UserMinimal>
- Use indexes:
@Entity(tableName = "users",
indices = [Index(value = ["email"], unique = true)])
data class User(
@PrimaryKey val id: Int,
@ColumnInfo(name = "email") val email: String,
@ColumnInfo(name = "name") val name: String
)
- Cache frequently accessed data:
class UserRepository(private val userDao: UserDao) {
private var cachedUsers: List<User>? = null
suspend fun getUsers(): List<User> {
cachedUsers?.let { return it }
return userDao.getAllUsers().also { cachedUsers = it }
}
suspend fun refreshUsers() {
cachedUsers = userDao.getAllUsers()
}
}
Optimization Technique | Performance Impact |
---|---|
Better Queries | 20-50% faster |
Using Indexes | Up to 10x faster |
Data Caching | Near-instant access |
Room with Other Android Tools
Room integrates well with other Android libraries:
- Room and ViewModel:
class UserViewModel(private val userDao: UserDao) : ViewModel() {
val allUsers: LiveData<List<User>> = userDao.getAllUsers()
}
// In UI
viewModel.allUsers.observe(this) { users ->
// Update UI with new user list
}
- Room with Paging:
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsers(): PagingSource<Int, User>
}
class UserViewModel(private val userDao: UserDao) : ViewModel() {
val userFlow = Pager(PagingConfig(pageSize = 20)) {
userDao.getAllUsers()
}.flow.cachedIn(viewModelScope)
}
// In UI
viewModel.userFlow.collectLatest { pagingData ->
userAdapter.submitData(pagingData)
}
- Room and WorkManager:
class DatabaseCleanupWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val database = AppDatabase.getInstance(applicationContext)
database.userDao().deleteOldUsers()
return Result.success()
}
}
// Schedule work
val cleanupWork = PeriodicWorkRequestBuilder<DatabaseCleanupWorker>(1, TimeUnit.DAYS)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"database_cleanup",
ExistingPeriodicWorkPolicy.KEEP,
cleanupWork
)
Room Tips and Common Mistakes
Good practices:
- Pre-populate database:
Room.databaseBuilder(context.applicationContext,
DataDatabase::class.java, "Sample.db")
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
ioThread {
getInstance(context).dataDao()
.insert(PREPOPULATE_DATA)
}
}
})
.build()
- Use DAO inheritance:
interface BaseDao<T> {
@Insert
fun insert(vararg obj: T)
}
@Dao
abstract class DataDao : BaseDao<Data>() {
@Query("SELECT * FROM Data")
abstract fun getData(): List<Data>
}
- Optimize queries:
- Use COUNT queries for counting
- Create separate GET queries for different use cases
- Use custom update queries for specific columns
Common mistakes:
- Ignoring database versioning
- Performing database operations on main thread
- Not using transactions for multiple operations
- Overusing hard deletes
Fixing Room problems:
- Migration issues: Provide migration paths or allow destructive migrations
- Performance problems: Use Database Inspector, create indexes, use @Relation annotation
- Relationship issues: Use @Embedded attribute or write custom queries
Moving from SQLite to Room
Steps to switch:
- Update dependencies
- Create entity classes
- Define DAOs
- Create database class
- Update database operations
- Handle migrations
Keeping data safe:
- Back up existing data
- Test migrations thoroughly
- Implement fallback strategies
- Consider gradual rollout
Conclusion
Room offers significant advantages over SQLite for Android developers:
- Less code
- Compile-time query validation
- Built-in functions for large datasets
- Structured approach with Entity-DAO-Repository model
Room integrates well with LiveData, ViewModel, and Coroutines, making it versatile for modern app development. While switching from SQLite requires careful planning, the long-term benefits in code maintainability and app performance make it worthwhile.
FAQs
Room offers several benefits for Android developers:
- Simplified database management
- Compile-time query checks
- Less code through ORM features
- Improved performance
Compared to SQLite:
Feature | Room | Direct SQLite |
---|---|---|
Query validation | Compile-time | Runtime |
Boilerplate code | Minimal | Extensive |
Object mapping | Automatic | Manual |
Migration support | Built-in | Manual |
Room's efficiency is evident in simple operations:
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAllUsers(): List<User>
}
This DAO method replaces multiple lines of SQLite code.
Tips for using Room:
- Query only necessary fields
- Use distinctUntilChanged() with Flow
- Implement UPSERT operations with OnConflictStrategy.IGNORE