close icon
daily.dev platform

Discover more from daily.dev

Personalized news feed, dev communities and search, much better than whatโ€™s out there. Maybe ;)

Start reading - Free forever
Start reading - Free forever
Continue reading >

Android Room Persistence Library: Complete Guide

Android Room Persistence Library: Complete Guide
Author
Nimrod Kramer
Related tags on daily.dev
toc
Table of contents
arrow-down

๐ŸŽฏ

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:

  1. Entities (database tables)
  2. DAOs (database operations)
  3. 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.

Room Architecture Basics

Room provides an abstract layer over SQLite with three main components:

  1. 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?
    )
    
  2. 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)
    }
    
  3. 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:

  1. App interacts with Database instance
  2. Database uses DAOs for operations
  3. 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:

  1. 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"
}
  1. Apply plugins in project-level build.gradle:
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}
  1. Sync project with Gradle files.

  2. 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"
}
  1. 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:

  1. Define parent entity:
@Entity(tableName = "schools")
data class School(
    @PrimaryKey val id: Int,
    val name: String
)
  1. 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
)
  1. 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:

  1. Increase version number
  2. 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")
    }
}
  1. 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:

  1. 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
}
  1. 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>
}
  1. 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)
    }
}
  1. 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

Kotlin Coroutines

Room works well with Kotlin Coroutines for efficient database operations:

  1. Add dependency:
dependencies {
    implementation "androidx.room:room-ktx:2.5.0"
}
  1. 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)
}
  1. Use in ViewModel:
class UserViewModel(private val userDao: UserDao) : ViewModel() {
    fun loadUsers() {
        viewModelScope.launch {
            val users = userDao.getAll()
            // Update UI with users
        }
    }
}
  1. 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

LiveData

Room integrates with LiveData and Flow for real-time data updates:

  1. 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
}
  1. 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:

  1. 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>
  1. 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
)
  1. 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:

  1. 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
}
  1. 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)
}
  1. 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:

  1. 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()
  1. 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>  
}
  1. Optimize queries:
    • Use COUNT queries for counting
    • Create separate GET queries for different use cases
    • Use custom update queries for specific columns

Common mistakes:

  1. Ignoring database versioning
  2. Performing database operations on main thread
  3. Not using transactions for multiple operations
  4. Overusing hard deletes

Fixing Room problems:

  1. Migration issues: Provide migration paths or allow destructive migrations
  2. Performance problems: Use Database Inspector, create indexes, use @Relation annotation
  3. Relationship issues: Use @Embedded attribute or write custom queries

Moving from SQLite to Room

SQLite

Steps to switch:

  1. Update dependencies
  2. Create entity classes
  3. Define DAOs
  4. Create database class
  5. Update database operations
  6. Handle migrations

Keeping data safe:

  1. Back up existing data
  2. Test migrations thoroughly
  3. Implement fallback strategies
  4. 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:

  1. Simplified database management
  2. Compile-time query checks
  3. Less code through ORM features
  4. 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

Related posts

Why not level up your reading with

Stay up-to-date with the latest developer news every time you open a new tab.

Read more