On this page
Raw SQLite on Android means string queries that fail at runtime, cursor juggling and manual threading. Room keeps the power of SQL but checks your queries at compile time, maps rows to Kotlin classes, and exposes results as Flows that update automatically when data changes.
Room's three parts
Setup
plugins { id("com.google.devtools.ksp") }
dependencies {
implementation("androidx.room:room-runtime:<room-version>")
implementation("androidx.room:room-ktx:<room-version>") // coroutines/Flow support
ksp("androidx.room:room-compiler:<room-version>")
testImplementation("androidx.room:room-testing:<room-version>")
}Entities
@Entity(
tableName = "notes",
indices = [Index("folderId"), Index("updatedAt")],
)
data class NoteEntity(
@PrimaryKey val id: String,
val title: String,
val body: String,
val folderId: String?,
val pinned: Boolean = false,
val updatedAt: Long, // epoch millis: simple, sortable
)
@Entity(tableName = "folders")
data class FolderEntity(@PrimaryKey val id: String, val name: String)DAOs
@Dao
interface NoteDao {
// Observed query: re-emits whenever the notes table changes
@Query("SELECT * FROM notes ORDER BY pinned DESC, updatedAt DESC")
fun observeAll(): Flow<List<NoteEntity>>
@Query("SELECT * FROM notes WHERE title LIKE '%' || :query || '%' OR body LIKE '%' || :query || '%'")
fun search(query: String): Flow<List<NoteEntity>>
@Query("SELECT * FROM notes WHERE id = :id")
suspend fun getById(id: String): NoteEntity?
@Upsert
suspend fun upsert(note: NoteEntity)
@Query("DELETE FROM notes WHERE id = :id")
suspend fun delete(id: String)
@Query("UPDATE notes SET pinned = NOT pinned WHERE id = :id")
suspend fun togglePinned(id: String)
}Room checks every SQL query against your schema at compile time, so a typo in a column name is a build error, not a crash. Suspend functions and Flow queries run off the main thread automatically.
The database
@Database(entities = [NoteEntity::class, FolderEntity::class], version = 2, exportSchema = true)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
abstract fun folderDao(): FolderDao
}
// Provide ONE instance for the whole app (e.g. Hilt @Singleton)
fun buildDatabase(context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2)
.build()Relations
data class FolderWithNotes(
@Embedded val folder: FolderEntity,
@Relation(parentColumn = "id", entityColumn = "folderId")
val notes: List<NoteEntity>,
)
@Dao
interface FolderDao {
@Transaction // runs the two underlying queries atomically
@Query("SELECT * FROM folders ORDER BY name")
fun observeFoldersWithNotes(): Flow<List<FolderWithNotes>>
}Type converters
class Converters {
@TypeConverter fun fromTags(tags: List<String>): String = tags.joinToString(",")
@TypeConverter fun toTags(value: String): List<String> = if (value.isEmpty()) emptyList() else value.split(",")
}For queryable lists, prefer a separate table over a converted string.
Migrations
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0")
db.execSQL("CREATE INDEX IF NOT EXISTS index_notes_updatedAt ON notes(updatedAt)")
}
}With exportSchema = true and a configured schema directory, Room writes JSON schema files you commit to Git. They enable auto-migrations for simple changes and migration testing. Never use fallbackToDestructiveMigration() for user data you can't re-download.
Testing a DAO
@RunWith(AndroidJUnit4::class)
class NoteDaoTest {
private lateinit var db: AppDatabase
private lateinit var dao: NoteDao
@Before fun setUp() {
db = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java).build()
dao = db.noteDao()
}
@After fun tearDown() = db.close()
@Test fun pinnedNotesComeFirst() = runTest {
dao.upsert(NoteEntity("1", "Old", "", null, pinned = false, updatedAt = 2))
dao.upsert(NoteEntity("2", "Pinned", "", null, pinned = true, updatedAt = 1))
assertEquals(listOf("2", "1"), dao.observeAll().first().map { it.id })
}
}Real-world uses
Offline cache
Room as the source of truth for API data. See Offline-First.
User-created content
Notes, tasks and expenses: data that must never be lost, with careful migrations.
Paged lists
Return PagingSource from DAOs for huge tables. See Paging 3.
Common mistakes
- Creating multiple database instances. Use a singleton.
- Missing indices on columns used in WHERE and ORDER BY clauses.
- Exposing entities to the UI. Map them to domain models.
allowMainThreadQueries()in production.- Destructive migration for important user data.
Frequently asked questions
Room or SQLDelight?
Room is Google's Android-first solution, and it now supports Kotlin Multiplatform too. SQLDelight generates Kotlin from SQL files and is also multiplatform. Both are solid choices.
How do I do full-text search?
Room supports SQLite FTS through @Fts4 entities, which are much faster than LIKE for large text.
Can I inspect the database while debugging?
Yes. Android Studio's App Inspection includes a Database Inspector for live queries.
Conclusion
Room gives you compile-time-checked SQL, Flow-based observation, suspend writes and managed migrations. Use a single instance, index your queries, export schemas, test DAOs and migrations, and map entities before they reach the UI.
Next step: decide what belongs in Room and what in DataStore with Room vs DataStore.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Networking, persistence and offline sync are where real apps get complicated. Practise building a data layer that handles slow networks, failures and caching, not just the happy path.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.