Room (Android Architecture Components)

Room (Android Room Persistence Library) is an Android-exclusive ORM (Object-Relational Mapping) library provided as part of Google's Android Jetpack project that "provides an abstraction layer over SQLite to allow for more robust database access." While leveraging the full power of SQLite, Room offers compile-time SQL query validation, type-safe database operations, and integration with LiveData and Kotlin Coroutines, providing a powerful data persistence solution for modern Android app development. Designed with a Kotlin First approach, it achieves natural integration with Android's MVVM architecture.

AndroidKotlinSQLiteORMJetpackDatabasePersistence

Library

Room (Android Architecture Components)

Overview

Room (Android Room Persistence Library) is an Android-exclusive ORM (Object-Relational Mapping) library provided as part of Google's Android Jetpack project that "provides an abstraction layer over SQLite to allow for more robust database access." While leveraging the full power of SQLite, Room offers compile-time SQL query validation, type-safe database operations, and integration with LiveData and Kotlin Coroutines, providing a powerful data persistence solution for modern Android app development. Designed with a Kotlin First approach, it achieves natural integration with Android's MVVM architecture.

Details

Room 2025 edition embodies the latest best practices in Android development through Kotlin Multiplatform support, Kotlin Symbol Processing (KSP) support, and complete Kotlin Coroutines integration. Through a clear architectural design with three main components - Entity, DAO, and Database - it provides complex database schema management and automatic migration functionality. Natural integration with LiveData, RxJava, and Kotlin Flow makes reactive programming and data binding easy to implement. Additionally, with related entity management, Full-Text Search (FTS) support, and encryption capabilities, it strongly supports enterprise-level Android application development.

Key Features

  • Compile-time Validation: Prevents runtime errors through compile-time SQL query validation
  • Type Safety: Completely type-safe database operations leveraging Kotlin's type system
  • Jetpack Integration: Complete integration with LiveData, ViewModel, and Data Binding
  • Automatic Migration: Automatic database schema change management
  • Kotlin Coroutines: Full support for asynchronous database operations
  • Reactive Queries: Automatic UI updates when data changes

Pros and Cons

Pros

  • Hides SQLite complexity while providing access to all features
  • Significantly improves app stability through compile-time query validation
  • Achieves automatic UI updates through LiveData integration
  • Efficient asynchronous processing with Kotlin Coroutines support
  • Natural MVVM architecture implementation through complete Android Jetpack integration
  • Long-term support by Google and active community

Cons

  • Android-exclusive, not usable for cross-platform development
  • Limited to SQLite, cannot use other database engines
  • Potential initialization time increase with large datasets
  • Complex queries may require relying on raw SQL
  • Flexible schema design like NoSQL is restricted
  • Risk of production environment issues due to poor migration design

Reference Pages

Code Examples

Setup

// app/build.gradle (Module: app)
android {
    compileSdk 34
    
    defaultConfig {
        minSdk 21
        targetSdk 34
    }
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
    
    kotlinOptions {
        jvmTarget = "17"
        freeCompilerArgs += ["-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"]
    }
}

dependencies {
    def room_version = "2.6.1"
    
    // Room core
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    
    // KSP (recommended - faster than kapt)
    ksp "androidx.room:room-compiler:$room_version"
    
    // Asynchronous processing and reactive programming
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0"
    
    // Testing
    testImplementation "androidx.room:room-testing:$room_version"
    androidTestImplementation "androidx.arch.core:core-testing:2.2.0"
}

// Add KSP plugin
plugins {
    id 'com.google.devtools.ksp' version '1.9.22-1.0.17'
}
// Set backup rules in AndroidManifest.xml
<application
    android:name=".MyApplication"
    android:allowBackup="true"
    android:fullBackupContent="@xml/backup_rules"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/Theme.MyApp">
    
    <!-- Room database configuration -->
    <meta-data
        android:name="room.schemaLocation"
        android:value="./schemas" />
</application>

Entity Definition

import androidx.room.*
import java.time.LocalDateTime
import java.time.OffsetDateTime

// Basic entity
@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    
    @ColumnInfo(name = "username")
    val username: String,
    
    @ColumnInfo(name = "email")
    val email: String,
    
    @ColumnInfo(name = "created_at", defaultValue = "CURRENT_TIMESTAMP")
    val createdAt: OffsetDateTime = OffsetDateTime.now(),
    
    @ColumnInfo(name = "is_active", defaultValue = "1")
    val isActive: Boolean = true
)

// Composite primary key and foreign key
@Entity(
    tableName = "posts",
    foreignKeys = [
        ForeignKey(
            entity = User::class,
            parentColumns = ["id"],
            childColumns = ["user_id"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ],
    indices = [
        Index(value = ["user_id"]),
        Index(value = ["title"], unique = false),
        Index(value = ["created_at"])
    ]
)
data class Post(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    
    @ColumnInfo(name = "user_id")
    val userId: Long,
    
    @ColumnInfo(name = "title")
    val title: String,
    
    @ColumnInfo(name = "content")
    val content: String,
    
    @ColumnInfo(name = "created_at")
    val createdAt: OffsetDateTime = OffsetDateTime.now(),
    
    @ColumnInfo(name = "updated_at")
    val updatedAt: OffsetDateTime? = null
)

// Embedded object
data class Address(
    val street: String,
    val city: String,
    val country: String,
    val postalCode: String
)

@Entity(tableName = "user_profiles")
data class UserProfile(
    @PrimaryKey val userId: Long,
    
    @ColumnInfo(name = "full_name")
    val fullName: String,
    
    @Embedded
    val address: Address,
    
    @ColumnInfo(name = "phone_number")
    val phoneNumber: String?
)

DAO Implementation

import androidx.room.*
import androidx.lifecycle.LiveData
import kotlinx.coroutines.flow.Flow

@Dao
interface UserDao {
    // Basic CRUD operations
    @Query("SELECT * FROM users ORDER BY created_at DESC")
    fun getAllUsers(): Flow<List<User>>
    
    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUserById(userId: Long): User?
    
    @Query("SELECT * FROM users WHERE username = :username")
    suspend fun getUserByUsername(username: String): User?
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User): Long
    
    @Insert
    suspend fun insertUsers(users: List<User>): List<Long>
    
    @Update
    suspend fun updateUser(user: User)
    
    @Delete
    suspend fun deleteUser(user: User)
    
    @Query("DELETE FROM users WHERE id = :userId")
    suspend fun deleteUserById(userId: Long)
    
    // Complex queries
    @Query("""
        SELECT * FROM users 
        WHERE is_active = 1 
        AND created_at >= :fromDate 
        ORDER BY username ASC
    """)
    fun getActiveUsersFrom(fromDate: OffsetDateTime): Flow<List<User>>
    
    @Query("""
        SELECT u.*, COUNT(p.id) as post_count 
        FROM users u 
        LEFT JOIN posts p ON u.id = p.user_id 
        GROUP BY u.id
        HAVING post_count > :minPosts
        ORDER BY post_count DESC
    """)
    suspend fun getUsersWithMinimumPosts(minPosts: Int): List<UserWithPostCount>
    
    // Aggregation operations
    @Query("SELECT COUNT(*) FROM users WHERE is_active = 1")
    suspend fun getActiveUserCount(): Int
    
    @Query("SELECT username FROM users WHERE created_at >= :date")
    suspend fun getRecentUsernames(date: OffsetDateTime): List<String>
}

@Dao
interface PostDao {
    @Query("SELECT * FROM posts WHERE user_id = :userId ORDER BY created_at DESC")
    fun getPostsByUserId(userId: Long): Flow<List<Post>>
    
    @Query("""
        SELECT p.*, u.username 
        FROM posts p 
        INNER JOIN users u ON p.user_id = u.id 
        WHERE p.title LIKE '%' || :searchTerm || '%' 
        OR p.content LIKE '%' || :searchTerm || '%'
        ORDER BY p.created_at DESC
    """)
    suspend fun searchPosts(searchTerm: String): List<PostWithUser>
    
    @Insert
    suspend fun insertPost(post: Post): Long
    
    @Update
    suspend fun updatePost(post: Post)
    
    @Transaction
    suspend fun updatePostWithTimestamp(post: Post) {
        val updatedPost = post.copy(updatedAt = OffsetDateTime.now())
        updatePost(updatedPost)
    }
}

// Related entities
data class UserWithPostCount(
    @Embedded val user: User,
    @ColumnInfo(name = "post_count") val postCount: Int
)

data class PostWithUser(
    @Embedded val post: Post,
    @ColumnInfo(name = "username") val username: String
)

data class UserWithPosts(
    @Embedded val user: User,
    @Relation(
        parentColumn = "id",
        entityColumn = "user_id"
    )
    val posts: List<Post>
)

Database Class

import androidx.room.*
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

@Database(
    entities = [User::class, Post::class, UserProfile::class],
    version = 3,
    exportSchema = true,
    autoMigrations = [
        AutoMigration(from = 1, to = 2),
        AutoMigration(
            from = 2, 
            to = 3, 
            spec = AppDatabase.Migration2to3::class
        )
    ]
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun postDao(): PostDao
    
    @DeleteColumn(tableName = "users", columnName = "temp_column")
    @RenameColumn(tableName = "posts", fromColumnName = "old_title", toColumnName = "title")
    class Migration2to3 : AutoMigrationSpec
    
    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"
                )
                .addMigrations(MIGRATION_1_2)
                .addCallback(DatabaseCallback())
                .fallbackToDestructiveMigration() // Development only
                .build()
                
                INSTANCE = instance
                instance
            }
        }
        
        // Manual migration example
        private val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("""
                    ALTER TABLE users 
                    ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1
                """)
                database.execSQL("""
                    CREATE INDEX index_users_is_active 
                    ON users(is_active)
                """)
            }
        }
    }
    
    private class DatabaseCallback : RoomDatabase.Callback() {
        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)
            // Initial data insertion processing
        }
        
        override fun onOpen(db: SupportSQLiteDatabase) {
            super.onOpen(db)
            // Database open processing
        }
    }
}

// Type converters
class Converters {
    @TypeConverter
    fun fromOffsetDateTime(value: OffsetDateTime?): Long? {
        return value?.toInstant()?.toEpochMilli()
    }
    
    @TypeConverter
    fun toOffsetDateTime(value: Long?): OffsetDateTime? {
        return value?.let {
            OffsetDateTime.ofInstant(
                java.time.Instant.ofEpochMilli(it),
                java.time.ZoneOffset.UTC
            )
        }
    }
    
    @TypeConverter
    fun fromStringList(value: List<String>): String {
        return value.joinToString(",")
    }
    
    @TypeConverter
    fun toStringList(value: String): List<String> {
        return if (value.isBlank()) emptyList() else value.split(",")
    }
}

ViewModel and Repository

import androidx.lifecycle.*
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject

// Repository
class UserRepository @Inject constructor(
    private val userDao: UserDao,
    private val postDao: PostDao
) {
    fun getAllUsers(): Flow<List<User>> = userDao.getAllUsers()
    
    suspend fun getUserById(userId: Long): User? = userDao.getUserById(userId)
    
    suspend fun insertUser(user: User): Long = userDao.insertUser(user)
    
    suspend fun updateUser(user: User) = userDao.updateUser(user)
    
    suspend fun deleteUser(user: User) = userDao.deleteUser(user)
    
    fun getUsersWithPosts(): Flow<List<UserWithPosts>> {
        return userDao.getAllUsers().map { users ->
            users.map { user ->
                val posts = postDao.getPostsByUserId(user.id).first()
                UserWithPosts(user, posts)
            }
        }
    }
    
    suspend fun searchUsers(query: String): List<User> {
        return userDao.getAllUsers().first().filter {
            it.username.contains(query, ignoreCase = true) ||
            it.email.contains(query, ignoreCase = true)
        }
    }
}

// ViewModel
class UserViewModel(private val repository: UserRepository) : ViewModel() {
    
    private val _searchQuery = MutableLiveData("")
    val searchQuery: LiveData<String> = _searchQuery
    
    private val _selectedUser = MutableLiveData<User?>()
    val selectedUser: LiveData<User?> = _selectedUser
    
    // Reactive user list
    val users: LiveData<List<User>> = repository.getAllUsers().asLiveData()
    
    // Search results
    val searchResults: LiveData<List<User>> = _searchQuery.switchMap { query ->
        if (query.isBlank()) {
            repository.getAllUsers().asLiveData()
        } else {
            liveData {
                emit(repository.searchUsers(query))
            }
        }
    }
    
    // Active user count
    val activeUserCount: LiveData<Int> = users.map { userList ->
        userList.count { it.isActive }
    }
    
    fun searchUsers(query: String) {
        _searchQuery.value = query
    }
    
    fun selectUser(user: User) {
        _selectedUser.value = user
    }
    
    fun createUser(username: String, email: String) {
        viewModelScope.launch {
            try {
                val newUser = User(
                    username = username,
                    email = email,
                    createdAt = OffsetDateTime.now()
                )
                repository.insertUser(newUser)
            } catch (e: Exception) {
                // Error handling
                _errorMessage.value = "Failed to create user: ${e.message}"
            }
        }
    }
    
    fun updateUser(user: User) {
        viewModelScope.launch {
            repository.updateUser(user)
        }
    }
    
    fun deleteUser(user: User) {
        viewModelScope.launch {
            repository.deleteUser(user)
        }
    }
    
    private val _errorMessage = MutableLiveData<String>()
    val errorMessage: LiveData<String> = _errorMessage
}

Transaction Processing

@Dao
interface TransactionDao {
    @Transaction
    suspend fun insertUserWithProfile(user: User, profile: UserProfile) {
        val userId = insertUser(user)
        val updatedProfile = profile.copy(userId = userId)
        insertProfile(updatedProfile)
    }
    
    @Transaction
    suspend fun transferPosts(fromUserId: Long, toUserId: Long) {
        val posts = getPostsByUserId(fromUserId)
        posts.forEach { post ->
            val transferredPost = post.copy(
                userId = toUserId,
                updatedAt = OffsetDateTime.now()
            )
            updatePost(transferredPost)
        }
    }
    
    @Insert
    suspend fun insertUser(user: User): Long
    
    @Insert
    suspend fun insertProfile(profile: UserProfile)
    
    @Query("SELECT * FROM posts WHERE user_id = :userId")
    suspend fun getPostsByUserId(userId: Long): List<Post>
    
    @Update
    suspend fun updatePost(post: Post)
}

// Transaction usage in Repository
class UserService @Inject constructor(
    private val database: AppDatabase,
    private val userDao: UserDao,
    private val transactionDao: TransactionDao
) {
    suspend fun createUserWithProfile(
        username: String,
        email: String,
        fullName: String,
        address: Address
    ): Result<Long> {
        return try {
            database.withTransaction {
                val user = User(username = username, email = email)
                val userId = userDao.insertUser(user)
                
                val profile = UserProfile(
                    userId = userId,
                    fullName = fullName,
                    address = address,
                    phoneNumber = null
                )
                
                transactionDao.insertProfile(profile)
                userId
            }
            Result.success(userId)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

Test Implementation

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.*
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class UserDaoTest {
    
    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()
    
    private lateinit var database: AppDatabase
    private lateinit var userDao: UserDao
    
    @Before
    fun setup() {
        database = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            AppDatabase::class.java
        )
        .allowMainThreadQueries()
        .build()
        
        userDao = database.userDao()
    }
    
    @After
    fun teardown() {
        database.close()
    }
    
    @Test
    fun insertAndGetUser() = runTest {
        // Given
        val user = User(
            username = "testuser",
            email = "[email protected]",
            createdAt = OffsetDateTime.now()
        )
        
        // When
        val userId = userDao.insertUser(user)
        val retrievedUser = userDao.getUserById(userId)
        
        // Then
        Assert.assertNotNull(retrievedUser)
        Assert.assertEquals(user.username, retrievedUser?.username)
        Assert.assertEquals(user.email, retrievedUser?.email)
    }
    
    @Test
    fun getAllUsers_returnsAllUsers() = runTest {
        // Given
        val users = listOf(
            User(username = "user1", email = "[email protected]"),
            User(username = "user2", email = "[email protected]"),
            User(username = "user3", email = "[email protected]")
        )
        
        // When
        userDao.insertUsers(users)
        val allUsers = userDao.getAllUsers().first()
        
        // Then
        Assert.assertEquals(3, allUsers.size)
        Assert.assertTrue(allUsers.any { it.username == "user1" })
        Assert.assertTrue(allUsers.any { it.username == "user2" })
        Assert.assertTrue(allUsers.any { it.username == "user3" })
    }
    
    @Test
    fun updateUser_updatesCorrectly() = runTest {
        // Given
        val user = User(username = "original", email = "[email protected]")
        val userId = userDao.insertUser(user)
        
        // When
        val updatedUser = user.copy(id = userId, username = "updated")
        userDao.updateUser(updatedUser)
        val retrievedUser = userDao.getUserById(userId)
        
        // Then
        Assert.assertEquals("updated", retrievedUser?.username)
        Assert.assertEquals("[email protected]", retrievedUser?.email)
    }
    
    @Test
    fun deleteUser_removesUser() = runTest {
        // Given
        val user = User(username = "toDelete", email = "[email protected]")
        val userId = userDao.insertUser(user)
        
        // When
        userDao.deleteUserById(userId)
        val deletedUser = userDao.getUserById(userId)
        
        // Then
        Assert.assertNull(deletedUser)
    }
    
    @Test
    fun searchQuery_worksCorrectly() = runTest {
        // Given
        val now = OffsetDateTime.now()
        val pastDate = now.minusDays(30)
        
        val oldUser = User(username = "old", email = "[email protected]", createdAt = pastDate)
        val newUser = User(username = "new", email = "[email protected]", createdAt = now)
        
        userDao.insertUser(oldUser)
        userDao.insertUser(newUser)
        
        // When
        val recentUsers = userDao.getActiveUsersFrom(now.minusDays(7)).first()
        
        // Then
        Assert.assertEquals(1, recentUsers.size)
        Assert.assertEquals("new", recentUsers[0].username)
    }
}