CS 3180 Mobile Application Development
Entity, DAO, Database
Entity
A data class annotated with @Entity.
Represents one database table.
Each instance is one row.
DAO (Data Access Object)
An interface annotated with @Dao.
Each function is one query.
Room generates the implementation.
SPEAKER NOTES:
Welcome to Week 12. Last week we covered DataStore for key-value preferences and file I/O for exports. Today we move to Room — the Jetpack library for structured relational data. If DataStore is a dictionary, Room is a spreadsheet. This is the persistence layer most Android apps depend on for their core data model.
SPEAKER NOTES:
Let students sit with this. They've seen how to persist preferences — but preferences are a handful of values. A golf scoring app has relationships: a tournament has players, each player has scores per hole, each hole has a par value. This demands a database, not a key-value store. Today's answer: Room.
SPEAKER NOTES:
Frame the session. Room is not a new database — it wraps SQLite, which has been on Android since version 1. What Room adds is: compile-time query verification, Kotlin coroutine integration, Flow support, and type safety. Students will write Kotlin with annotations; Room generates the SQL and the implementation.
SPEAKER NOTES:
Before diving into Room's API, establish why raw SQLite is painful. Students who've done JDBC or Android's Cursor API will recognize these problems immediately.
SPEAKER NOTES:
This is the raw Android SQLite API. Every query involves a Cursor — a pointer into the result set that you move manually. Every column must be retrieved by index or name at runtime, and if you spell the column name wrong, it crashes at runtime. There's no compile-time checking. And everything runs on whatever thread you called it from — no built-in threading safety.
SPEAKER NOTES:
This alone is worth the migration. In raw SQLite, a misspelled column name crashes at runtime when a user is using the app. Room runs your SQL through SQLite at compile time — the build fails instead.
SPEAKER NOTES:
Room generates the Cursor-to-object mapping from your annotations. You get your data class back — no getInt(), getString(), getColumnIndex() boilerplate.
SPEAKER NOTES:
Every DAO function can be a suspend function (runs on Dispatchers.IO automatically) or return a Flow (reactive — emits whenever the data changes). This connects directly to the ViewModel and Compose patterns students already know.
SPEAKER NOTES:
When your schema changes (new column, new table), Room manages the migration through a versioned migration system. Without it, schema changes either crash the app or silently delete all user data.
SPEAKER NOTES:
Room doesn't change what SQLite does — it makes SQLite ergonomic from Kotlin. The database file on disk is standard SQLite, readable by any SQLite tool. That means everything you know about SQL still applies. Room just removes the boilerplate.
SPEAKER NOTES:
Room has exactly three building blocks. Every Room database, no matter how complex, is built from these three. Learn them once, apply everywhere.
SPEAKER NOTES:
Draw this on the board. Entity → table. DAO → queries. Database → container. Students often confuse DAO and Database. Rule of thumb: if it defines the shape of data, it's an Entity. If it defines operations on data, it's a DAO. The Database class just wires them together and provides the entry point.
SPEAKER NOTES:
Before writing any Room code, we need the library. Room requires the main library plus the KSP annotation processor, which generates the DAO implementations at compile time.
SPEAKER NOTES:
The KSP plugin (Kotlin Symbol Processing) replaces the older KAPT. It's faster and is the current recommendation. The room-compiler artifact is what reads your @Entity and @Dao annotations and generates implementation classes at build time. The room-ktx artifact adds the suspend and Flow support — don't omit it. Version 2.6.1 is compatible with Kotlin 1.9+ and AGP 8.x.
SPEAKER NOTES:
An Entity is a data class with an @Entity annotation. Room maps each property to a database column automatically. Let's build the entities for the Hope Foundation golf app.
SPEAKER NOTES:
Walk through each annotation. @Entity tells Room "this is a table." tableName is optional — if omitted, Room uses the class name (lowercased). @PrimaryKey is required — every table must have one. autoGenerate means you don't supply the ID; SQLite auto-increments it. The default id = 0 is conventional — 0 signals "new record, assign an ID."
SPEAKER NOTES:
By default Room converts camelCase property names to column names as-is. @ColumnInfo(name = "first_name") maps the Kotlin firstName property to the first_name column — a common convention for SQL. The index on tournament_id is important: we'll query all players for a tournament frequently. Indexing speeds this query from O(n) to O(log n) as the table grows. Add indexes on any column you filter or join on.
SPEAKER NOTES:
ForeignKey declares a relationship to the Player table. onDelete = CASCADE means if a Player is deleted, all their Scores are automatically deleted too — no orphaned records. Room enforces referential integrity through SQLite foreign key constraints. The indices list speeds up the most common queries: "all scores for a player" and "all scores for a tournament." Both will appear in our DAOs.
SPEAKER NOTES:
These rules come from production experience. A missing index on a foreign key column means full table scans on every related query — fine with 10 rows, painful with 10,000. SQL naming convention (snake_case) keeps your schema readable in Database Inspector and raw SQL tools. Entities are dumb data — move any logic to the DAO or repository.
SPEAKER NOTES:
DAOs define what you can do with the data. Each DAO function is an annotated Kotlin function — Room generates the SQL implementation. You write the what; Room writes the how.
SPEAKER NOTES:
Four annotation types: @Query for SELECT, @Insert for INSERT, @Update for UPDATE, @Delete for DELETE. Notice the return types: getAllTournaments() returns Flow<List<Tournament>> — it will re-emit whenever any tournament row changes. getTournamentById() returns Tournament? — nullable because the ID might not exist. insert() returns Long — the auto-generated row ID. Room validates the SQL in @Query at compile time.
SPEAKER NOTES:
This is the most common design decision in Room. Flow is reactive — collect it in the ViewModel with stateIn(), and the UI recomposes whenever the data changes. Use Flow for any data that appears in a list or screen that should refresh automatically. Suspend is one-shot — call it once, get the answer, move on. Use suspend for writes (insert/update/delete) and for lookups you only need at a specific moment (like loading detail data when navigating to a screen).
SPEAKER NOTES:
onConflict specifies what happens when a row with the same primary key already exists. REPLACE is the most common — it deletes the old row and inserts the new one. IGNORE silently skips the insert. ABORT throws an SQLiteConstraintException. Bulk insert (List<Score>) is much more efficient than calling insertScore() in a loop — Room batches the inserts in one transaction. Use it whenever you're inserting multiple records at startup or during sync.
SPEAKER NOTES:
Parameters in @Query use the :parameterName syntax — Room validates that the parameter name matches the function parameter at compile time. Multi-line queries use triple-quoted strings. Aggregate functions (SUM, COUNT, AVG) work exactly as in SQL. The result is nullable (Int?) because SUM of zero rows returns NULL in SQL. Room maps NULL to a Kotlin nullable type.
SPEAKER NOTES:
@Query isn't just for SELECT. DELETE and UPDATE in @Query are more flexible than @Delete and @Update — they can target multiple rows by condition without loading any objects first. deleteScoresForTournament() deletes every score for a tournament in one SQL operation — far more efficient than loading all scores and deleting one-by-one. The targeted UPDATE is useful for in-app score editing where you know exactly which row to change.
SPEAKER NOTES:
The Database class is the top-level container that ties entities and DAOs together. You declare it once; it's a singleton in the app.
SPEAKER NOTES:
Three required elements of @Database: entities list (every table in the schema), version (integer, increment when schema changes), exportSchema (set true to generate a JSON schema file — useful for reviewing migrations in code review). The abstract functions return DAOs — Room generates the implementations. The companion object holds the singleton: @Volatile ensures visibility across threads, synchronized prevents double-initialization. This is the standard Android Room singleton pattern.
SPEAKER NOTES:
When you change the schema (add a column, rename a table, add a table), you MUST increment the version number AND provide a Migration. Without a migration, Room throws an exception on upgrade — it won't silently drop and recreate the database (which would delete all user data). The migration gets the old SupportSQLiteDatabase and executes raw SQL. ALTER TABLE ADD COLUMN is the most common migration. Wednesday's demo will use Database Inspector to verify migrations.
SPEAKER NOTES:
DAOs are the database interface. But ViewModel shouldn't talk to DAOs directly — that would couple business logic to database implementation details. The Repository sits between them.
SPEAKER NOTES:
The Repository pattern is not required by Room — it's an architectural pattern. But it's highly recommended and is part of the official Android architecture guidance. The key benefit is testability: a ViewModel that depends on a Repository interface can be tested with a fake implementation, no database needed.
SPEAKER NOTES:
The repository is a thin wrapper here — one function per DAO function. In a real app, the repository might combine multiple data sources (local Room + remote API) or add caching logic. The key point: the ViewModel doesn't import anything from androidx.room. If you later swap Room for a different database, only the repository changes.
SPEAKER NOTES:
This is identical to the DataStore ViewModel pattern from last week. The repository's Flow becomes a StateFlow via stateIn(). SharingStarted.WhileSubscribed(5_000) stops collection 5 seconds after the last collector leaves — saves resources when the screen is in the background. Write operations (insert, delete) are suspend functions called inside viewModelScope.launch. The composable calls the regular (non-suspend) ViewModel functions; it has no idea that coroutines are involved.
SPEAKER NOTES:
Manual DI: create the database singleton, construct the repository with the DAO, pass the repository to the ViewModel factory. This pattern works fine for assignments and moderate-sized projects. For larger projects, you'd use Hilt (Google's DI library) to automate all of this. The viewModel() factory lambda is the viewModelFactory DSL from lifecycle-viewmodel-compose.
SPEAKER NOTES:
Let's see the complete data flow from database to screen for the Tournament List feature.
SPEAKER NOTES:
Draw this on the board. This is the full reactive pipeline. The key insight: when any row in the tournaments table changes (insert, update, delete), Room invalidates the Flow. The Flow re-emits the new list. The StateFlow updates. The composable recomposes. No polling, no manual refresh buttons, no explicit notification. The data is always current.
SPEAKER NOTES:
Standard pattern. key = { it.id } gives LazyColumn stable identity for each item — enables smooth animations and prevents unnecessary recompositions when items are reordered. The key should always be the primary key from Room. The Scaffold FAB will open an add-tournament dialog. When the user saves, vm.addTournament() inserts into Room, the Flow re-emits, the LazyColumn updates — all automatically.
SPEAKER NOTES:
The smallest unit: annotate a data class and Room creates the table. PrimaryKey is the mandatory unique identifier.
SPEAKER NOTES:
The query layer. Four annotation types: @Query, @Insert, @Update, @Delete. Room generates implementations from these at compile time. SQL errors surface at build time, not runtime.
SPEAKER NOTES:
One per app. Lists all entities and provides abstract functions returning DAOs. Built as a singleton with the companion object pattern.
SPEAKER NOTES:
DAO functions returning Flow are the key to the reactive data pipeline. The UI never polls — it observes. Room notifies observers when data changes.
SPEAKER NOTES:
The architectural boundary. ViewModel knows repositories, not DAOs. This enables testing with fakes and makes the data source swappable.
SPEAKER NOTES:
Never change the schema without a migration — it crashes the app on upgrade. Wednesday's session covers complex queries (JOIN, aggregate), database relationships (@Relation), and Database Inspector for live debugging. The lab this week implements a full Room database for tournament scores.
SPEAKER NOTES:
Lab 8 is the Android Developers CodeLab on Room — it's guided and walks through the same concepts from today in a structured project. The Chapter 10 Quiz covers the material from today's lecture and Wednesday's demo. Remind students that the project alpha (Week 13) requires a working Room database — today's content is directly applicable.