Skip to main content

Android Room Persistence Library: Complete Guide

Nimrod Kramer Nimrod Kramer
Link copied!
Android Room Persistence Library: Complete Guide
Quick take

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
Read more, every new tab

Posts like this, on every new tab.

daily.dev curates a feed of articles ranked against what you actually care about. Free forever.

Link copied!