DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Room Database Complete Guide: Local Data Persistence in Compose

Room Database Complete Guide: Local Data Persistence in Compose

Room is Android's recommended persistence library for local SQLite database access. It provides an abstraction layer on top of SQLite while maintaining full control over the database.

Core Components

Entity: Define Your Data Model

Entities represent tables in your database:

@Entity(tableName = "tasks")
data class TaskEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val title: String,
    val description: String,
    val completed: Boolean = false,
    val createdAt: Long = System.currentTimeMillis()
)
Enter fullscreen mode Exit fullscreen mode

Use @ColumnInfo for custom column names, @Ignore to exclude fields from persistence.

DAO: Data Access Operations

DAOs define the methods for accessing database operations:

@Dao
interface TaskDao {
    @Insert
    suspend fun insertTask(task: TaskEntity)

    @Update
    suspend fun updateTask(task: TaskEntity)

    @Delete
    suspend fun deleteTask(task: TaskEntity)

    // Flow for real-time observation
    @Query("SELECT * FROM tasks ORDER BY createdAt DESC")
    fun getAllTasksFlow(): Flow<List<TaskEntity>>

    // Suspend for one-shot queries
    @Query("SELECT * FROM tasks WHERE id = :id")
    suspend fun getTaskById(id: Int): TaskEntity?
}
Enter fullscreen mode Exit fullscreen mode

Key distinction:

  • Use Flow<T> for continuous observation of data changes
  • Use suspend fun for one-shot queries that don't need observation
  • Room automatically manages threading

Database: Singleton Setup

Create an abstract database class:

@Database(
    entities = [TaskEntity::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                Room.databaseBuilder(
                    context,
                    AppDatabase::class.java,
                    "app_database.db"
                )
                .build()
                .also { INSTANCE = it }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Always use a singleton pattern to prevent multiple database instances.

Repository Pattern

Repositories abstract data access logic from the UI layer:

class TaskRepository(
    private val taskDao: TaskDao
) {
    val allTasks: Flow<List<TaskEntity>> = taskDao.getAllTasksFlow()

    suspend fun addTask(task: TaskEntity) {
        taskDao.insertTask(task)
    }

    suspend fun updateTask(task: TaskEntity) {
        taskDao.updateTask(task)
    }

    suspend fun getTask(id: Int): TaskEntity? {
        return taskDao.getTaskById(id)
    }
}
Enter fullscreen mode Exit fullscreen mode

ViewModel Integration

Connect your repository to UI through ViewModels:

@HiltViewModel
class TaskViewModel @Inject constructor(
    private val repository: TaskRepository
) : ViewModel() {
    val tasks: StateFlow<List<TaskEntity>> = repository.allTasks
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    fun addTask(title: String, description: String) {
        viewModelScope.launch {
            repository.addTask(
                TaskEntity(title = title, description = description)
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Migrations

Handle schema changes with migrations:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            "ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 0"
        )
    }
}

Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .addMigrations(MIGRATION_1_2)
    .build()
Enter fullscreen mode Exit fullscreen mode

TypeConverters

Convert complex types to/from database-compatible types:

class DateTypeConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

@Database(entities = [TaskEntity::class], version = 1)
@TypeConverters(DateTypeConverter::class)
abstract class AppDatabase : RoomDatabase() {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

KSP Setup

Configure Room's Kotlin Symbol Processing for annotation processing:

# build.gradle.kts
plugins {
    id("com.google.devtools.ksp")
}

dependencies {
    implementation("androidx.room:room-runtime:2.6.1")
    ksp("androidx.room:room-compiler:2.6.1")
}
Enter fullscreen mode Exit fullscreen mode

KSP is faster than KAPT and is now recommended by Google.

Best Practices

  1. Single Database Instance: Use companion object and lazy initialization
  2. Flow for Observation: Always use Flow for queries that UI observes
  3. Suspend for One-Shots: Use suspend fun for single-value queries
  4. Repository Pattern: Don't expose DAO directly to UI
  5. Testing: Use createInMemoryDatabaseBuilder() for unit tests
  6. Schema Export: Set exportSchema = true and commit schemas for version control

8 Android App Templates → https://myougatheaxo.gumroad.com

Top comments (0)