Room database for local persistence

Alex Chen Jan 2026
2 tabs
package com.example.myapp.data.local

import android.content.Context
import androidx.room.*
import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@Database(
    entities = [PostEntity::class, CommentEntity::class],
    version = 2,
    exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun postDao(): PostDao
    abstract fun commentDao(): CommentDao

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

        fun getDatabase(
            context: Context,
            scope: CoroutineScope
        ): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                )
                    .addMigrations(MIGRATION_1_2)
                    .addCallback(DatabaseCallback(scope))
                    .build()
                INSTANCE = instance
                instance
            }
        }

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

    private class DatabaseCallback(
        private val scope: CoroutineScope
    ) : RoomDatabase.Callback() {
        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)
            INSTANCE?.let { database ->
                scope.launch {
                    populateDatabase(database.postDao())
                }
            }
        }

        suspend fun populateDatabase(postDao: PostDao) {
            // Prepopulate with sample data
            val samplePost = PostEntity(
                id = 1,
                title = "Welcome Post",
                body = "Welcome to the app!",
                authorId = 1,
                createdAt = System.currentTimeMillis()
            )
            postDao.insert(samplePost)
        }
    }
}

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

    @TypeConverter
    fun dateToTimestamp(date: java.util.Date?): Long? {
        return date?.time
    }
}
2 files · kotlin Explain with highlit

Room provides an abstraction layer over SQLite for compile-time verified database access. I define entities with @Entity annotation, specifying table structure and relationships. DAOs (Data Access Objects) marked with @Dao contain query methods using @Query, @Insert, @Update, and @Delete. Room generates implementation at compile time. The @Database annotation creates the database instance. Migrations handle schema changes safely. Room works seamlessly with coroutines and Flow for reactive queries. Type converters handle complex types like Date or List. Foreign keys enforce relationships. Prepopulating databases uses createFromAsset(). Room's compile-time verification catches SQL errors before runtime.