How Room Works Internally — Code Generation, InvalidationTracker, WAL, Threading, and Schema Validation
You define an @Entity, write a @Dao interface, annotate a @Database class, and Room magically gives you a working database. But what’s actually happening? How does Room turn your annotations into SQL? Where does the generated code live? How does Room know to re-emit a Flow when data changes? Understanding the internals makes you better at debugging, writing efficient queries, and answering interview questions. This guide opens up Room’s hood.
The Mental Model — What Room Actually Is
// Room is NOT a database — it's a LAYER on top of SQLite
//
// YOUR CODE ROOM (generated) SQLITE
// (annotations) (real implementation) (actual database)
//
// @Entity Article → CREATE TABLE articles ... → articles.db file
// @Dao ArticleDao → ArticleDao_Impl.java → SQL queries
// @Database AppDatabase → AppDatabase_Impl.java → SQLiteOpenHelper
//
// You write: interfaces and data classes with annotations
// Room generates: full Java/Kotlin implementation classes
// SQLite does: the actual disk I/O and query execution
//
// The generation happens at COMPILE TIME via KSP (or previously KAPT)
// The generated code is plain, readable Java — no reflection, no magic
// You can read it yourself in: build/generated/ksp/debug/java/...
What Room Generates — The Real Code
For @Database — AppDatabase_Impl
// YOUR CODE:
@Database(entities = [ArticleEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun articleDao(): ArticleDao
}
// ROOM GENERATES: AppDatabase_Impl (simplified)
class AppDatabase_Impl extends AppDatabase {
// This is a REAL CLASS generated by Room's KSP processor
// You can find it in: build/generated/ksp/debug/java/.../AppDatabase_Impl.java
private volatile ArticleDao_Impl _articleDao;
@Override
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config) {
// SupportSQLiteOpenHelper is an ABSTRACT CLASS from androidx.sqlite
// It wraps Android's SQLiteOpenHelper with a cleaner API
// Room uses this to create/open/migrate the database file
return config.sqliteOpenHelperFactory.create(
SupportSQLiteOpenHelper.Configuration.builder(config.context)
.name(config.name) // database file name
.callback(new RoomOpenHelper(
config,
new RoomOpenHelper.Delegate(1) {
// version = 1 (from @Database annotation)
@Override
public void createAllTables(SupportSQLiteDatabase db) {
// Called when database is FIRST CREATED
// Room generates ALL the CREATE TABLE statements:
db.execSQL("CREATE TABLE IF NOT EXISTS `articles` ("
+ "`id` TEXT NOT NULL, "
+ "`title` TEXT NOT NULL, "
+ "`content` TEXT NOT NULL, "
+ "`published_at` INTEGER NOT NULL, "
+ "PRIMARY KEY(`id`))");
// Room also creates the room_master_table:
db.execSQL("CREATE TABLE IF NOT EXISTS `room_master_table` ("
+ "`id` INTEGER PRIMARY KEY, "
+ "`identity_hash` TEXT)");
db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash)"
+ " VALUES(42, 'abc123def456...')");
// identity_hash is a HASH of your schema
// Room checks this on open — if it doesn't match,
// the schema changed without a migration → crash or destructive fallback
}
}
))
.build()
);
}
@Override
public ArticleDao articleDao() {
if (_articleDao != null) return _articleDao;
synchronized(this) {
if (_articleDao == null) {
_articleDao = new ArticleDao_Impl(this);
// Creates the DAO implementation ONCE (lazy singleton)
}
return _articleDao;
}
}
}
// KEY INSIGHT: Room generates a REAL class that extends your abstract class
// No reflection, no proxy — just generated Java code compiled into your APK
For @Dao — ArticleDao_Impl
// YOUR CODE:
@Dao
interface ArticleDao {
@Query("SELECT * FROM articles ORDER BY published_at DESC")
fun getAllArticles(): Flow<List<ArticleEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertArticles(articles: List<ArticleEntity>)
@Query("SELECT * FROM articles WHERE id = :articleId")
suspend fun getArticleById(articleId: String): ArticleEntity?
}
// ROOM GENERATES: ArticleDao_Impl (simplified)
class ArticleDao_Impl implements ArticleDao {
private final RoomDatabase __db;
// Reference to the database — used for getting the SupportSQLiteDatabase
ArticleDao_Impl(RoomDatabase __db) {
this.__db = __db;
}
// ═══ @Query returning Flow ═══════════════════════════════════════
@Override
public Flow<List<ArticleEntity>> getAllArticles() {
final String _sql = "SELECT * FROM articles ORDER BY published_at DESC";
// Room extracts the SQL from @Query annotation at compile time
// SQL is VALIDATED at compile time — typo in table name? → compile error!
return CoroutinesRoom.createFlow(
__db,
false, // inTransaction
new String[]{"articles"}, // TABLES TO OBSERVE ← this is the magic!
new Callable<List<ArticleEntity>>() {
@Override
public List<ArticleEntity> call() {
// This Callable runs the ACTUAL SQL query
final Cursor _cursor = DBUtil.query(__db, _sql, false, null);
// DBUtil.query() is an INTERNAL FUNCTION from Room
// Executes the SQL and returns a Cursor
try {
final List<ArticleEntity> _result = new ArrayList<>();
while (_cursor.moveToNext()) {
final ArticleEntity _item = new ArticleEntity(
_cursor.getString(0), // id
_cursor.getString(1), // title
_cursor.getString(2), // content
_cursor.getLong(3) // published_at
);
// Room reads EACH COLUMN by index
// Column order matches the CREATE TABLE statement
_result.add(_item);
}
return _result;
} finally {
_cursor.close();
}
}
}
);
// CoroutinesRoom.createFlow() is the KEY function:
// 1. Runs the query immediately → emits first result
// 2. Registers with InvalidationTracker to watch the "articles" table
// 3. When ANY write happens on "articles" → re-runs the query → emits new result
// This is HOW Room Flows are reactive!
}
// ═══ @Insert ═════════════════════════════════════════════════════
@Override
public Object insertArticles(List<ArticleEntity> articles, Continuation cont) {
// suspend functions get a Continuation parameter (CPS transformation)
return CoroutinesRoom.execute(
__db,
true, // inTransaction — writes are always in a transaction
new Callable<Void>() {
@Override
public Void call() {
__db.beginTransaction();
try {
// Room generates INSERT statements:
final SupportSQLiteStatement _stmt = __db.compileStatement(
"INSERT OR REPLACE INTO `articles` (`id`,`title`,`content`,`published_at`) VALUES (?,?,?,?)"
);
for (ArticleEntity item : articles) {
_stmt.bindString(1, item.getId());
_stmt.bindString(2, item.getTitle());
_stmt.bindString(3, item.getContent());
_stmt.bindLong(4, item.getPublishedAt());
_stmt.executeInsert();
}
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
return null;
}
},
cont // the Continuation — resumes the coroutine when done
);
}
}
// KEY INSIGHTS:
// 1. Room generates REAL implementation classes — no reflection
// 2. SQL strings are extracted from annotations AT COMPILE TIME
// 3. Cursor reading is column-by-column, index-based (fast)
// 4. suspend functions use CoroutinesRoom.execute() which handles threading
// 5. Flow queries use CoroutinesRoom.createFlow() which watches for table changes
CoroutinesRoom — How Threading Works
// When you call a suspend DAO function, Room uses CoroutinesRoom to handle threading
// CoroutinesRoom is an INTERNAL CLASS from room-ktx
// For WRITE operations (insert, update, delete):
CoroutinesRoom.execute(db, inTransaction = true, callable, continuation)
// 1. Switches to Room's transaction dispatcher
// 2. Begins a transaction
// 3. Runs your SQL
// 4. Commits the transaction
// 5. Notifies InvalidationTracker ("articles table changed!")
// 6. Resumes the coroutine via continuation
// For READ operations (suspend @Query):
CoroutinesRoom.execute(db, inTransaction = false, callable, continuation)
// 1. Switches to Room's query dispatcher
// 2. Runs the query
// 3. Maps cursor to objects
// 4. Resumes the coroutine with the result
// Room's internal dispatchers:
// - Transaction dispatcher: single-threaded, ensures write ordering
// Only ONE write operation at a time (serialized)
// - Query dispatcher: uses the Architecture Components IO executor
// Multiple reads can run in parallel
//
// This is WHY you don't need withContext(Dispatchers.IO) for Room:
// Room's suspend functions ALREADY switch to background threads internally
// Adding withContext(IO) is harmless but redundant
// For FLOW operations:
CoroutinesRoom.createFlow(db, inTransaction, tableNames, callable)
// 1. Runs the query immediately → emits first result
// 2. Registers an Observer on the InvalidationTracker for the specified tables
// 3. When a table is invalidated (write happened) → re-runs the query
// 4. Emits the new result to all collectors
// 5. When the Flow collector cancels → unregisters the observer
InvalidationTracker — How Room Flows Are Reactive
This is Room’s most important internal mechanism — it’s how Room knows when to re-emit a Flow:
// The InvalidationTracker is a CLASS inside RoomDatabase
// It watches tables for changes and notifies observers
// HOW IT WORKS:
//
// 1. Room creates a HIDDEN table called room_table_modification_log
// (or uses SQLite's built-in invalidation in newer versions)
//
// 2. For EVERY write operation (INSERT, UPDATE, DELETE),
// Room records WHICH TABLES were modified
// This happens inside the transaction — zero extra cost
//
// 3. After the transaction COMMITS, Room checks:
// "Were any observed tables modified?"
//
// 4. If YES → notifies all registered observers for those tables
// Observer callback → re-runs the Flow query → emits new result
//
// ═══ VISUAL FLOW ═════════════════════════════════════════════════════
//
// DAO.insertArticle(article)
// │
// ↓
// Room begins transaction
// │
// ↓
// INSERT INTO articles VALUES (...)
// │
// ↓
// Room marks "articles" table as INVALIDATED
// │
// ↓
// Room commits transaction
// │
// ↓
// InvalidationTracker checks: who's watching "articles"?
// │
// ↓
// Found: getAllArticles() Flow is watching!
// │
// ↓
// Re-runs: SELECT * FROM articles ORDER BY published_at DESC
// │
// ↓
// Flow emits the new list to all collectors
// │
// ↓
// UI updates automatically
// IMPORTANT: Room invalidates at the TABLE level, not ROW level
// If you insert article #123, Room re-runs ALL queries watching "articles"
// Even a query for "WHERE category = 'tech'" re-runs, even if #123 is 'sports'
// This is a trade-off: simple and reliable, but may cause unnecessary re-queries
// For most apps, this is fine — queries are fast and results are usually different
SupportSQLiteDatabase — The Abstraction Layer
// Room doesn't use Android's SQLiteDatabase directly
// It uses SupportSQLiteDatabase — an INTERFACE from androidx.sqlite
//
// WHY an abstraction?
// 1. Testability — in tests, you can swap in an in-memory database
// 2. Future-proofing — could switch to a different SQLite implementation
// 3. Consistency — cleaner API than Android's raw SQLiteDatabase
// The layer stack:
//
// Your @Dao interface
// ↓
// ArticleDao_Impl (generated)
// ↓
// RoomDatabase (your AppDatabase)
// ↓
// SupportSQLiteDatabase (INTERFACE — abstraction)
// ↓
// FrameworkSQLiteDatabase (IMPLEMENTATION — wraps Android's SQLiteDatabase)
// ↓
// Android SQLiteDatabase (the real database)
// ↓
// SQLite C library (native code, the actual engine)
// SupportSQLiteOpenHelper is an ABSTRACT CLASS
// It wraps Android's SQLiteOpenHelper and provides:
// - onCreate() → Room generates CREATE TABLE statements
// - onUpgrade() → Room runs your migrations
// - onOpen() → Room validates schema (identity_hash check)
// You can access the raw database for advanced operations:
val rawDb: SupportSQLiteDatabase = appDatabase.openHelper.writableDatabase
// openHelper is a PROPERTY on RoomDatabase — the SupportSQLiteOpenHelper
// writableDatabase is a PROPERTY — opens/returns the database
// ⚠️ Rarely needed — use DAO queries instead
WAL Mode — Write-Ahead Logging
// WAL (Write-Ahead Logging) is a SQLite journaling mode
// Room enables WAL by DEFAULT — and it's critical for performance
// WITHOUT WAL (old journal mode):
// ❌ Writers BLOCK readers — can't read while writing
// ❌ Only ONE operation at a time (serialize everything)
//
// WITH WAL (Room's default):
// ✅ Writers and readers work SIMULTANEOUSLY
// ✅ Reads don't block writes, writes don't block reads
// ✅ Multiple readers at the same time
//
// HOW WAL WORKS:
//
// Normal mode:
// Write → modify the database file directly → readers must wait
//
// WAL mode:
// Write → append to a SEPARATE log file (WAL file) → database file untouched
// Read → reads from database file (which is consistent) → not blocked by writes
// Checkpoint → WAL file merged back into database file (happens automatically)
//
// The database has THREE files:
// app.db → the main database file
// app.db-wal → the write-ahead log (pending writes)
// app.db-shm → shared memory file (for coordinating WAL)
// Room enables WAL automatically:
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.build()
// WAL is ON by default
// Disable WAL (rare — only if you need compatibility with old tools):
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
// JournalMode is an ENUM on RoomDatabase: AUTOMATIC, WRITE_AHEAD_LOGGING, TRUNCATE
// AUTOMATIC → Room decides (WAL on most devices)
// WRITE_AHEAD_LOGGING → force WAL
// TRUNCATE → old journal mode (no concurrent reads/writes)
.build()
// WHY THIS MATTERS:
// With WAL, your Flow queries can re-run while an insert is happening
// Without WAL, the Flow query blocks until the insert finishes
// WAL = smoother UI, especially during background syncs
RoomDatabase.Callback — Hooks into the Lifecycle
// RoomDatabase.Callback is an ABSTRACT CLASS
// It provides hooks for database creation, opening, and destructive migration
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// Called ONCE when the database is FIRST CREATED (not on every open)
// Use for: inserting default/seed data
// db is the raw SupportSQLiteDatabase — you can run SQL here
db.execSQL("INSERT INTO categories (id, name) VALUES ('tech', 'Technology')")
db.execSQL("INSERT INTO categories (id, name) VALUES ('science', 'Science')")
// Pre-populate with default categories
// Or trigger a coroutine to load seed data:
// CoroutineScope(Dispatchers.IO).launch {
// database.articleDao().insertAll(defaultArticles)
// }
}
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
// Called EVERY TIME the database is opened
// Use for: enabling features, running maintenance
// ⚠️ Runs on the thread that opens the database — keep it fast
// Enable foreign key enforcement (not on by default in SQLite):
db.execSQL("PRAGMA foreign_keys = ON")
}
override fun onDestructiveRecreation(db: SupportSQLiteDatabase) {
super.onDestructiveRecreation(db)
// Called when fallbackToDestructiveMigration deletes and recreates
// Use for: logging, analytics ("user lost data!")
}
})
.build()
Pre-populated Databases — createFromAsset and createFromFile
// Sometimes you want to SHIP a database with your app
// (dictionary app, recipe app, map data, reference data)
// Instead of loading data on first launch, bundle a pre-built .db file
// ═══ createFromAsset — ship database in APK assets ═══════════════════
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.createFromAsset("databases/prepopulated.db")
// createFromAsset() is a FUNCTION on RoomDatabase.Builder
// "databases/prepopulated.db" is a file in your assets/ folder
// On first launch: Room copies this file AS the database
// Subsequent launches: uses the existing database (doesn't re-copy)
.build()
// The asset database MUST match your @Entity schema exactly!
// Same table names, same column names, same types
// Room validates the schema on open — mismatch → crash
// HOW TO CREATE the asset database:
// 1. Enable schema export: exportSchema = true in @Database
// 2. Build the app → Room generates schema JSON in schemas/1.json
// 3. Use the schema to create a .db file (via sqlitebrowser or a script)
// 4. Place the .db file in app/src/main/assets/databases/
// ═══ createFromFile — copy from device storage ═══════════════════════
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.createFromFile(File("/sdcard/Download/imported.db"))
// createFromFile() is a FUNCTION on RoomDatabase.Builder
// Copies from the specified file on first creation
// Use for: importing databases from downloads or backups
.build()
// COMBINING with migrations — pre-populated + updates:
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.createFromAsset("databases/v1.db") // start from asset
.addMigrations(MIGRATION_1_2, MIGRATION_2_3) // apply schema updates
.build()
// First install: copies v1.db, runs migrations to reach current version
// This lets you ship a large initial dataset and still evolve the schema
Schema Validation — The Identity Hash
// Room generates a HASH of your entire database schema:
// - Table names, column names, column types, primary keys, indices, foreign keys
// This hash is stored in room_master_table
// On every database OPEN:
// 1. Room reads the identity_hash from room_master_table
// 2. Room computes the hash of the CURRENT @Entity classes (compiled into the app)
// 3. Compares the two:
// MATCH → schema is consistent → proceed normally
// MISMATCH → schema changed without proper migration!
// → If fallbackToDestructiveMigration → delete and recreate
// → If no fallback → CRASH with IllegalStateException
// This is WHY you get the error:
// "Room cannot verify the data integrity. Looks like you've changed schema
// but forgot to update the version number."
//
// The fix: increment @Database(version = N+1) and add a migration
// The identity_hash also prevents:
// - Accidentally using a database file from a different schema version
// - Pre-populated databases that don't match the current schema
// - Corruption from manual SQL changes that Room doesn't know about
Room’s Compile-Time SQL Validation
// One of Room's BEST features: SQL is validated AT COMPILE TIME
// ❌ Typo in table name:
@Query("SELECT * FROM artcles") // "artcles" not "articles"
// COMPILE ERROR: "There is a problem with the query: no such table: artcles"
// ❌ Wrong column name:
@Query("SELECT * FROM articles WHERE ttle = :title") // "ttle" not "title"
// COMPILE ERROR: "no such column: ttle"
// ❌ Type mismatch:
@Query("SELECT * FROM articles WHERE published_at = :date")
fun getByDate(date: String): List<ArticleEntity>
// ⚠️ WARNING: "published_at" is INTEGER but you're comparing with String
// ❌ Missing parameter binding:
@Query("SELECT * FROM articles WHERE id = :articleId AND category = :cat")
fun get(articleId: String): ArticleEntity // missing "cat" parameter!
// COMPILE ERROR: "unused parameter: cat" or "missing parameter"
// HOW Room validates:
// 1. KSP reads the @Query SQL string
// 2. Room parses the SQL using its own SQLite parser
// 3. Room checks table names against @Entity classes
// 4. Room checks column names against @Entity properties
// 5. Room checks parameter bindings (:param) against function parameters
// 6. Room checks return type compatibility
// All at COMPILE TIME — errors show up as build errors, not runtime crashes
// This is a HUGE advantage over raw SQLite:
// Raw SQL: db.rawQuery("SELCT * FORM artcles", null) → runtime crash
// Room: @Query("SELCT * FORM artcles") → compile error → caught before release
Performance Internals
// How Room optimises database access:
//
// 1. PREPARED STATEMENTS
// Room pre-compiles SQL statements with compileStatement()
// The compiled statement is reused for repeated operations
// INSERT in a loop: compile once, bind + execute N times (much faster than N compiles)
//
// 2. CURSOR WINDOW MANAGEMENT
// SQLite returns data through a CursorWindow (a shared memory buffer)
// Room reads columns by INDEX (not by name) — faster column lookup
// Room closes cursors in finally blocks — no cursor leaks
//
// 3. TRANSACTION BATCHING
// @Insert with a List → Room wraps ALL inserts in ONE transaction
// Without transaction: 1000 inserts = 1000 disk flushes (slow!)
// With transaction: 1000 inserts = 1 disk flush (fast!)
//
// 4. INVALIDATION BATCHING
// Multiple writes in quick succession → ONE invalidation notification
// Room batches invalidation checks to avoid re-querying for every single write
//
// 5. LAZY DAO CREATION
// DAOs are created on first access, not on database creation
// appDatabase.articleDao() → lazy singleton pattern
//
// PERFORMANCE TIPS:
// ✅ Use @Transaction for multiple related writes
// ✅ Insert lists, not individual items: insertAll(list) not loop { insert(item) }
// ✅ Use indices on columns you filter/sort by
// ✅ Use LIMIT in queries if you only need a few rows
// ❌ Don't SELECT * if you only need a few columns (use a projection)
// ❌ Don't call suspend DAO functions in a tight loop — batch operations
Where to Find Generated Code
// Room's generated code is real, readable Java/Kotlin
// Looking at it helps you understand Room and debug issues
// Location:
// app/build/generated/ksp/debug/java/com/example/your/package/
// ├── AppDatabase_Impl.java ← database implementation
// ├── ArticleDao_Impl.java ← DAO implementation
// └── ...
// In Android Studio:
// 1. Build the project (Build → Make Project)
// 2. Navigate to your DAO interface
// 3. Cmd+Click (or Ctrl+Click) on a @Query method
// 4. Android Studio offers to navigate to the generated implementation
//
// OR:
// Open the "Build" tool window → look for generated sources
//
// Reading the generated code helps you:
// ✅ Understand exactly what SQL Room executes
// ✅ Debug unexpected query results
// ✅ Verify that your migration SQL matches Room's expectations
// ✅ Learn how Room handles threading and transactions
Common Mistakes to Avoid
Mistake 1: Thinking Room uses reflection
// ❌ "Room is slow because it uses reflection to read annotations"
// WRONG! Room generates code at COMPILE TIME via KSP
// At runtime, it's just regular method calls — zero reflection
// Room is as fast as hand-written SQLite code (because it IS generated code)
Mistake 2: Wrapping Room calls in withContext(Dispatchers.IO)
// ❌ Redundant — Room's suspend functions already switch threads
val articles = withContext(Dispatchers.IO) {
dao.getAllArticlesOnce() // Room already handles threading!
}
// ✅ Just call directly — Room switches internally
val articles = dao.getAllArticlesOnce()
// CoroutinesRoom.execute() handles the thread switching for you
Mistake 3: Not understanding table-level invalidation
// ❌ Expecting Row-level updates — "I only inserted ONE article, why does
// my getAllArticles() Flow re-emit ALL articles?"
// ANSWER: Room invalidates at the TABLE level, not row level
// ANY write to "articles" table → ALL queries watching "articles" re-run
// This is by design — simple, reliable, correct (even if slightly wasteful)
// ✅ This is fine for 99% of apps — queries are fast
// If it's a problem: use more specific queries or custom invalidation
Mistake 4: Not using transaction for multi-step operations
// ❌ Without transaction — each insert is a separate disk flush
for (article in articles) {
dao.insert(article) // disk flush for EACH article — very slow!
}
// ✅ Room batches list inserts in a transaction automatically
dao.insertAll(articles) // ONE transaction, ONE disk flush — fast!
// ✅ For custom multi-step: use @Transaction
@Transaction
suspend fun replaceAll(newArticles: List<ArticleEntity>) {
deleteAll()
insertAll(newArticles)
// Both run in ONE transaction — all or nothing
}
Mistake 5: Ignoring the schema export
// ❌ exportSchema = false — can't test migrations, no schema history
@Database(entities = [...], version = 3, exportSchema = false)
// ✅ exportSchema = true (default) — Room saves schema JSON for each version
@Database(entities = [...], version = 3, exportSchema = true)
// Schemas saved in: schemas/1.json, schemas/2.json, schemas/3.json
// Required for: auto-migrations and MigrationTestHelper
// Add to .gitignore? NO — commit these, they're your migration history
Summary
- Room generates real implementation classes at compile time via KSP — no reflection, no runtime overhead
- AppDatabase_Impl (generated class) extends your abstract database, creates tables, validates schema with identity hash
- ArticleDao_Impl (generated class) implements your DAO interface with actual SQL execution and cursor reading
- SupportSQLiteDatabase (interface from androidx.sqlite) is Room’s abstraction over Android’s SQLiteDatabase
- SupportSQLiteOpenHelper (abstract class) wraps SQLiteOpenHelper — handles creation, migration, and schema validation
- CoroutinesRoom (internal class from room-ktx) handles threading for suspend functions — you don’t need
withContext(IO) - InvalidationTracker (class inside RoomDatabase) watches tables for changes — this is how Room Flows are reactive
- Invalidation is at TABLE level, not row level — any write to a table re-triggers all queries watching it
- WAL mode (Write-Ahead Logging) is enabled by default — allows concurrent reads and writes
- RoomDatabase.Callback (abstract class) provides hooks:
onCreate()for seed data,onOpen()for maintenance createFromAsset()(function on Builder) ships a pre-populated database in your APK assetscreateFromFile()(function on Builder) copies a database from device storage on first creation- Room validates SQL at compile time — typos in table/column names are build errors, not runtime crashes
- Room uses prepared statements, transaction batching, and lazy DAO creation for performance
- The identity hash in room_master_table ensures schema consistency — mismatch means migration is needed
- Generated code is in
build/generated/ksp/— read it to understand exactly what Room does
Room’s magic is no magic at all — it’s code generation. KSP reads your annotations, generates Java classes that execute SQL, manage cursors, handle threading, and track table invalidation. Understanding this makes you better at using Room: you know why Flows re-emit (InvalidationTracker), why suspend is safe without withContext (CoroutinesRoom), why schema changes crash (identity hash), and where to look when things go wrong (generated code).
Happy coding!
Comments (0)
Sign in to leave a comment.
No comments yet. Be the first to share your thoughts.