CS 3180 Mobile Application Development

Week 12: Room Database

CS 3180 — Mobile Application Development

Entities · DAOs · Database · Repository

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

When a Dictionary Isn't Enough

"A golf app tracks 12 tournaments, 80+ players, and hundreds of holes of score data. DataStore stores that last-opened tournament ID. Where does all the actual data live?"

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

This Session

When DataStore isn't enough — structured, relational, queryable data.

  • What Room is — SQLite with a Kotlin-friendly API
  • The three components — Entity, DAO, Database
  • Entities — annotate data classes to define tables
  • DAOs — define queries as Kotlin functions
  • Repository — bridge to the ViewModel layer

By the end: a working database layer for tournament and score data.

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

The Problem Room Solves

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Raw SQLite: The Old Way

val db = this.writableDatabase
val cursor = db.rawQuery(
    "SELECT * FROM scores WHERE player_id = ?",
    arrayOf(playerId.toString())
)

while (cursor.moveToNext()) {
    val score = cursor.getInt(cursor.getColumnIndexOrThrow("strokes"))
    // ... manually map every column
}
cursor.close()

Every query: open cursor → map columns → close cursor. No type safety.

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

What Room Adds

  • Compile-time SQL verification — typos in queries are caught at build time
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

What Room Adds

  • Compile-time SQL verification — typos in queries are caught at build time
  • Type-safe mapping — annotated data classes replace manual Cursor mapping
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

What Room Adds

  • Compile-time SQL verification — typos in queries are caught at build time
  • Type-safe mapping — annotated data classes replace manual Cursor mapping
  • Coroutine and Flow support — queries are suspend functions or Flow streams
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

What Room Adds

  • Compile-time SQL verification — typos in queries are caught at build time
  • Type-safe mapping — annotated data classes replace manual Cursor mapping
  • Coroutine and Flow support — queries are suspend functions or Flow streams
  • Migration support — structured schema upgrades with version tracking
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

What Room Adds

  • Compile-time SQL verification — typos in queries are caught at build time
  • Type-safe mapping — annotated data classes replace manual Cursor mapping
  • Coroutine and Flow support — queries are suspend functions or Flow streams
  • Migration support — structured schema upgrades with version tracking
  • Database Inspector integration — inspect, query, and modify the database from Android Studio

Room is SQLite with Kotlin idioms. Not a replacement — a wrapper.

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Room's Three Components

Week 12 Monday: Room Database — Entities & DAOs
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.

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Entity, DAO, Database

Database
An abstract class annotated with @Database.
Lists which entities and DAOs it contains.
Provides access to DAO instances.

YourDatabase
├── Entity A → Table A
├── Entity B → Table B
├── DAO A    → queries on A
└── DAO B    → queries on B
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Step 1: Add Dependencies

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Room Dependencies

// build.gradle.kts (app module)
plugins {
    id("com.google.devtools.ksp")   // add KSP plugin
}

dependencies {
    val roomVersion = "2.6.1"
    implementation("androidx.room:room-runtime:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion")   // coroutine extensions
    ksp("androidx.room:room-compiler:$roomVersion")          // annotation processor
}

Three pieces: runtime, Kotlin extensions, compiler (KSP).

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Step 2: Define Entities

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

The Simplest Entity

@Entity(tableName = "tournaments")
data class Tournament(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,

    val name: String,
    val date: String,        // ISO-8601: "2026-06-15"
    val location: String,
    val isActive: Boolean = false
)
  • @Entity → this class maps to a table
  • @PrimaryKey → the unique row identifier
  • autoGenerate = true → SQLite assigns the ID on insert
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Column Customization

@Entity(tableName = "players")
data class Player(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,

    @ColumnInfo(name = "first_name")
    val firstName: String,

    @ColumnInfo(name = "last_name")
    val lastName: String,

    val handicap: Double = 0.0,

    @ColumnInfo(name = "tournament_id", index = true)
    val tournamentId: Int
)

@ColumnInfo — custom column name and index = true for fast lookups.

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Score Entity

@Entity(
    tableName = "scores",
    foreignKeys = [
        ForeignKey(
            entity = Player::class,
            parentColumns = ["id"],
            childColumns = ["player_id"],
            onDelete = ForeignKey.CASCADE
        )
    ],
    indices = [Index("player_id"), Index("tournament_id")]
)
data class Score(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,

    @ColumnInfo(name = "player_id")   val playerId: Int,
    @ColumnInfo(name = "tournament_id") val tournamentId: Int,
    val hole: Int,
    val par: Int,
    val strokes: Int
)
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Entity Best Practices

  • Every entity needs exactly one @PrimaryKey
  • Use autoGenerate = true for internal records; omit it for records with natural IDs (like course codes)
  • Add indexes on any column you filter or join on in queries
  • Use @ColumnInfo(name = ...) to follow SQL naming conventions (snake_case)
  • Don't put business logic in entities — they're plain data containers
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Step 3: Define DAOs

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

DAO Basics

@Dao
interface TournamentDao {

    @Query("SELECT * FROM tournaments ORDER BY date DESC")
    fun getAllTournaments(): Flow<List<Tournament>>

    @Query("SELECT * FROM tournaments WHERE id = :id")
    suspend fun getTournamentById(id: Int): Tournament?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(tournament: Tournament): Long

    @Update
    suspend fun update(tournament: Tournament)

    @Delete
    suspend fun delete(tournament: Tournament)
}
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Flow vs Suspend in DAOs

Return Flow<T> when:

  • The UI should stay updated
  • Multiple things might change this data
  • Used in a list screen
@Query("SELECT * FROM tournaments")
fun getAllTournaments(): Flow<List<Tournament>>

Return suspend (one-shot) when:

  • You need the value once
  • For lookups before navigation
  • For write operations
@Query("SELECT * FROM tournaments WHERE id = :id")
suspend fun getById(id: Int): Tournament?
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

@Insert Options

@Dao
interface ScoreDao {

    // Insert one record — returns the new row ID
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertScore(score: Score): Long

    // Insert many records at once (more efficient than looping)
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertScores(scores: List<Score>)

    // Insert, ignore if ID already exists
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insertIfNew(score: Score): Long
}

OnConflictStrategy: REPLACE (overwrite), IGNORE (skip), ABORT (throw)

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

@Query with Parameters

@Dao
interface ScoreDao {

    // Filter by player
    @Query("SELECT * FROM scores WHERE player_id = :playerId ORDER BY hole ASC")
    fun getScoresForPlayer(playerId: Int): Flow<List<Score>>

    // Filter by tournament and player
    @Query("""
        SELECT * FROM scores
        WHERE tournament_id = :tournamentId AND player_id = :playerId
        ORDER BY hole ASC
    """)
    fun getScoresForPlayerInTournament(
        tournamentId: Int,
        playerId: Int
    ): Flow<List<Score>>

    // Aggregate: total strokes for a player in a tournament
    @Query("""
        SELECT SUM(strokes) FROM scores
        WHERE tournament_id = :tournamentId AND player_id = :playerId
    """)
    fun getTotalStrokes(tournamentId: Int, playerId: Int): Flow<Int?>
}
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

@Query with DELETE and UPDATE

@Dao
interface ScoreDao {

    // Delete by query (more flexible than @Delete)
    @Query("DELETE FROM scores WHERE tournament_id = :tournamentId")
    suspend fun deleteScoresForTournament(tournamentId: Int)

    // Update specific columns without loading the full object
    @Query("""
        UPDATE scores SET strokes = :strokes
        WHERE tournament_id = :tournamentId
          AND player_id = :playerId
          AND hole = :hole
    """)
    suspend fun updateStrokesForHole(
        tournamentId: Int,
        playerId: Int,
        hole: Int,
        strokes: Int
    )
}
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Step 4: Create the Database

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

The Database Class

@Database(
    entities = [Tournament::class, Player::class, Score::class],
    version = 1,
    exportSchema = true
)
abstract class GolfDatabase : RoomDatabase() {

    abstract fun tournamentDao(): TournamentDao
    abstract fun playerDao(): PlayerDao
    abstract fun scoreDao(): ScoreDao

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

        fun getInstance(context: Context): GolfDatabase {
            return INSTANCE ?: synchronized(this) {
                Room.databaseBuilder(
                    context.applicationContext,
                    GolfDatabase::class.java,
                    "golf_database"
                ).build().also { INSTANCE = it }
            }
        }
    }
}
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Schema Versioning

@Database(
    entities = [Tournament::class, Player::class, Score::class],
    version = 2,    // bumped from 1 — schema changed
    exportSchema = true
)
abstract class GolfDatabase : RoomDatabase() {

    // ... dao functions ...

    companion object {
        fun getInstance(context: Context): GolfDatabase {
            return INSTANCE ?: synchronized(this) {
                Room.databaseBuilder(context.applicationContext, GolfDatabase::class.java, "golf_database")
                    .addMigrations(MIGRATION_1_2)
                    .build()
                    .also { INSTANCE = it }
            }
        }

        val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL("ALTER TABLE players ADD COLUMN email TEXT NOT NULL DEFAULT ''")
            }
        }
    }
}
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Step 5: The Repository

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Why a Repository?

Without Repository

ViewModel → DAO → Room

  • ViewModel knows about Room
  • Swapping databases requires ViewModel changes
  • Hard to test: ViewModel needs a real DB

With Repository

ViewModel → Repository → DAO → Room

  • ViewModel knows only about data contracts
  • Repository hides the data source
  • Easy to test: swap real repo for a fake
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Tournament Repository

class TournamentRepository(private val dao: TournamentDao) {

    // Expose the Flow directly — ViewModel will use stateIn()
    val allTournaments: Flow<List<Tournament>> = dao.getAllTournaments()

    suspend fun getTournamentById(id: Int): Tournament? =
        dao.getTournamentById(id)

    suspend fun insertTournament(tournament: Tournament): Long =
        dao.insert(tournament)

    suspend fun updateTournament(tournament: Tournament) =
        dao.update(tournament)

    suspend fun deleteTournament(tournament: Tournament) =
        dao.delete(tournament)
}
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

ViewModel with Room

class TournamentViewModel(
    private val repository: TournamentRepository
) : ViewModel() {

    val tournaments: StateFlow<List<Tournament>> = repository.allTournaments
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )

    fun addTournament(name: String, date: String, location: String) {
        viewModelScope.launch {
            repository.insertTournament(
                Tournament(name = name, date = date, location = location)
            )
        }
    }

    fun deleteTournament(tournament: Tournament) {
        viewModelScope.launch {
            repository.deleteTournament(tournament)
        }
    }
}
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Wiring It Together: Dependency Injection

// Simple manual DI (for small apps):
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val database = GolfDatabase.getInstance(this)
        val repository = TournamentRepository(database.tournamentDao())

        setContent {
            val viewModel: TournamentViewModel = viewModel(
                factory = viewModelFactory {
                    initializer { TournamentViewModel(repository) }
                }
            )
            GolfAppTheme { TournamentListScreen(viewModel) }
        }
    }
}
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Putting It All Together

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Complete Data Flow

GolfDatabase (singleton)
    ↓ provides
TournamentDao
    ↓ wrapped by
TournamentRepository
    ↓ exposed as Flow<List<Tournament>>
TournamentViewModel
    ↓ converted to StateFlow via stateIn()
TournamentListScreen (Composable)
    ↓ collectAsState()
LazyColumn of tournament cards

Room writes → Flow emits → StateFlow updates → Compose recomposes.

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Full Tournament List Screen

@Composable
fun TournamentListScreen(vm: TournamentViewModel = viewModel()) {
    val tournaments by vm.tournaments.collectAsState()

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = { /* show add dialog */ }) {
                Icon(Icons.Default.Add, contentDescription = "Add Tournament")
            }
        }
    ) { padding ->
        LazyColumn(contentPadding = padding) {
            items(tournaments, key = { it.id }) { tournament ->
                TournamentCard(
                    tournament = tournament,
                    onDelete = { vm.deleteTournament(it) }
                )
            }
        }
    }
}
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Summary

  • Entity — data class + @Entity + @PrimaryKey = a table
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Summary

  • Entity — data class + @Entity + @PrimaryKey = a table
  • DAO — interface + @Dao + annotated functions = database operations
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Summary

  • Entity — data class + @Entity + @PrimaryKey = a table
  • DAO — interface + @Dao + annotated functions = database operations
  • Database — abstract class + @Database = the container and entry point
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Summary

  • Entity — data class + @Entity + @PrimaryKey = a table
  • DAO — interface + @Dao + annotated functions = database operations
  • Database — abstract class + @Database = the container and entry point
  • Flow returns — reactive queries that update the UI automatically
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Summary

  • Entity — data class + @Entity + @PrimaryKey = a table
  • DAO — interface + @Dao + annotated functions = database operations
  • Database — abstract class + @Database = the container and entry point
  • Flow returns — reactive queries that update the UI automatically
  • Repository — decouples ViewModel from Room implementation
Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Summary

  • Entity — data class + @Entity + @PrimaryKey = a table
  • DAO — interface + @Dao + annotated functions = database operations
  • Database — abstract class + @Database = the container and entry point
  • Flow returns — reactive queries that update the UI automatically
  • Repository — decouples ViewModel from Room implementation
  • Migrations — increment version + provide Migration when schema changes

Wednesday: Complex queries, relationships, and Database Inspector.

Week 12 Monday: Room Database — Entities & DAOs
CS 3180 Mobile Application Development

Due This Week

Item Due
Lab 8: Room for Data Persistence CodeLab Friday 11:59 PM
Chapter 10 Quiz Sunday 11:59 PM
zyBook Ch 10 activities Sunday 11:59 PM

Project Alpha: Core Room database layer due with next week's milestone.

Week 12 Monday: Room Database — Entities & DAOs

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.