CS 3180 Mobile Application Development

Week 12 Wednesday: Room in Depth

CS 3180 — Mobile Application Development

Complex Queries · Relationships · Transactions · Database Inspector

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

What Does "Real" Data Look Like?

"The Hope Foundation app stores 80 players across 12 tournaments, each with 18 hole scores. How do you query the leaderboard — top 10 players by total strokes — without loading all 17,000 rows into memory?"

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

This Session

Pushing Room further — from basic CRUD to production-ready queries.

  • Filtering and sorting — parameterized queries, IN clauses, dynamic ordering
  • Aggregation — COUNT, SUM, AVG directly in SQL
  • Transactions — atomic multi-step operations
  • Relationships@Embedded and @Relation for joined data
  • Database Inspector — live query and debug in Android Studio
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Complex Queries

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Filtering with Parameters

@Dao
interface PlayerDao {

    // Filter by tournament
    @Query("SELECT * FROM players WHERE tournament_id = :tournamentId ORDER BY last_name ASC")
    fun getPlayersForTournament(tournamentId: Int): Flow<List<Player>>

    // Filter by multiple IDs at once
    @Query("SELECT * FROM players WHERE id IN (:playerIds)")
    suspend fun getPlayersByIds(playerIds: List<Int>): List<Player>

    // Text search (case-insensitive LIKE)
    @Query("SELECT * FROM players WHERE first_name LIKE '%' || :query || '%' OR last_name LIKE '%' || :query || '%'")
    fun searchPlayers(query: String): Flow<List<Player>>
}
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Aggregation Queries

@Dao
interface ScoreDao {

    // 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?>

    // Average score per hole for a tournament
    @Query("""
        SELECT AVG(strokes - par) FROM scores
        WHERE tournament_id = :tournamentId
    """)
    fun getAverageDifferential(tournamentId: Int): Flow<Double?>

    // Count of completed holes for a player
    @Query("""
        SELECT COUNT(*) FROM scores
        WHERE tournament_id = :tournamentId AND player_id = :playerId
    """)
    fun getCompletedHoleCount(tournamentId: Int, playerId: Int): Flow<Int>
}
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

The Leaderboard Query

@Dao
interface ScoreDao {

    @Query("""
        SELECT
            p.id          AS playerId,
            p.first_name  AS firstName,
            p.last_name   AS lastName,
            SUM(s.strokes) AS totalStrokes,
            SUM(s.strokes - s.par) AS totalVsPar,
            COUNT(s.id)   AS holesPlayed
        FROM players p
        INNER JOIN scores s ON s.player_id = p.id
        WHERE s.tournament_id = :tournamentId
        GROUP BY p.id
        ORDER BY totalStrokes ASC
        LIMIT :limit
    """)
    fun getLeaderboard(tournamentId: Int, limit: Int = 10): Flow<List<LeaderboardEntry>>
}
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Mapping Custom Query Results

// Room maps query results to any data class — not just entities
data class LeaderboardEntry(
    val playerId: Int,
    val firstName: String,
    val lastName: String,
    val totalStrokes: Int,
    val totalVsPar: Int,
    val holesPlayed: Int
)
  • Property names must match the AS aliases in the query exactly
  • Room validates the mapping at compile time
  • This is not an @Entity — it's a query result model
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Transactions

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Why Transactions Matter

// Without transaction: if this crashes after insert, scores exist without a player
suspend fun addPlayerWithScores(player: Player, scores: List<Score>) {
    val playerId = playerDao.insert(player)
    scoreDao.insertScores(scores.map { it.copy(playerId = playerId.toInt()) })
}

// With @Transaction: all inserts succeed or all are rolled back
@Transaction
suspend fun addPlayerWithScores(player: Player, scores: List<Score>) {
    val playerId = playerDao.insert(player)
    scoreDao.insertScores(scores.map { it.copy(playerId = playerId.toInt()) })
}

@Transaction — mark a function to run atomically in one database transaction.

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Batch Operations with Transactions

@Dao
interface ScoreDao {

    @Transaction
    suspend fun replaceAllScores(tournamentId: Int, newScores: List<Score>) {
        deleteScoresForTournament(tournamentId)
        insertScores(newScores)
    }

    @Query("DELETE FROM scores WHERE tournament_id = :tournamentId")
    suspend fun deleteScoresForTournament(tournamentId: Int)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertScores(scores: List<Score>)
}

Useful for sync operations — replace local data with fresh server data atomically.

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Database Relationships

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Option 1: @Embedded

data class Address(
    val street: String,
    val city: String,
    val state: String
)

@Entity(tableName = "players")
data class Player(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val firstName: String,
    val lastName: String,
    @Embedded val address: Address    // flattened into the players table
)

@Embedded flattens the nested object's fields into the parent table's columns.

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Option 2: @Relation (One-to-Many)

// Define the relationship as a data class (not an entity)
data class TournamentWithPlayers(
    @Embedded val tournament: Tournament,

    @Relation(
        parentColumn = "id",
        entityColumn = "tournament_id"
    )
    val players: List<Player>
)

// Query in DAO — always mark @Relation queries with @Transaction
@Dao
interface TournamentDao {
    @Transaction
    @Query("SELECT * FROM tournaments WHERE id = :tournamentId")
    fun getTournamentWithPlayers(tournamentId: Int): Flow<TournamentWithPlayers?>
}
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Full Score Summary

// Player with all their scores for a tournament
data class PlayerScoreSummary(
    @Embedded val player: Player,

    @Relation(
        parentColumn = "id",
        entityColumn = "player_id"
    )
    val scores: List<Score>
)

@Dao
interface PlayerDao {
    @Transaction
    @Query("""
        SELECT * FROM players
        WHERE tournament_id = :tournamentId
        ORDER BY last_name ASC
    """)
    fun getPlayerScoreSummaries(tournamentId: Int): Flow<List<PlayerScoreSummary>>
}
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Database Inspector

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Opening Database Inspector

How to open:

  1. Run your app on emulator or device
  2. In Android Studio: View → Tool Windows → App Inspection
  3. Select your running process
  4. Click the Database Inspector tab

What you see:

  • All Room databases in the app
  • Each database's tables
  • Live row counts
  • Table contents as a grid
  • A query console
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Browsing Table Contents

App Inspection → Database Inspector → golf_database

├── tournaments
│   ├── id: 1  name: "Spring Classic"  date: "2026-04-05"  location: "Pebble Beach"
│   └── id: 2  name: "Charity Open"   date: "2026-06-15"  location: "Torrey Pines"
│
├── players
│   ├── id: 1  first_name: "Alice"  last_name: "Chen"   tournament_id: 1
│   └── id: 2  first_name: "Bob"    last_name: "Martinez" tournament_id: 1
│
└── scores
    ├── id: 1  player_id: 1  tournament_id: 1  hole: 1  par: 4  strokes: 5
    └── ...

Click any table → see live rows. Rows update as the app writes data.

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Running Queries in Inspector

-- Find all scores for player 1:
SELECT * FROM scores WHERE player_id = 1 ORDER BY hole ASC;

-- Leaderboard preview:
SELECT p.first_name, p.last_name, SUM(s.strokes) AS total
FROM players p
INNER JOIN scores s ON s.player_id = p.id
WHERE s.tournament_id = 1
GROUP BY p.id
ORDER BY total ASC;

-- Verify a specific score was written:
SELECT * FROM scores WHERE tournament_id = 1 AND player_id = 2 AND hole = 5;

The query console runs any valid SQLite SQL.

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Inspector: Live Data Toggle

Keep database connections open:

In Inspector settings, enable
"Keep database connections open"

This prevents Inspector from being disconnected when Room's connection pool recycles.

What to watch for:

  • Row count changes in real time
  • Flow re-emission triggers visible as updates
  • Foreign key violations surface as error rows
  • Null vs empty string in optional columns
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Live Demo: Golf App Database

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

What We're Building

GolfDatabase (Room)
├── tournaments   ← @Entity: Tournament
├── players       ← @Entity: Player
└── scores        ← @Entity: Score (FK → players.id)

DAOs
├── TournamentDao  ← getAllTournaments(): Flow, insert, delete
├── PlayerDao      ← getPlayersForTournament(): Flow, insert
└── ScoreDao       ← getScoresForPlayer(): Flow, insertScores(), leaderboard query

ViewModels
├── TournamentViewModel ← exposes tournament list, add/delete
└── ScorecardViewModel  ← exposes player scores, leaderboard
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Live: ScoreDao

@Dao
interface ScoreDao {

    @Transaction
    @Query("SELECT * FROM players WHERE tournament_id = :tournamentId ORDER BY last_name ASC")
    fun getPlayerScoreSummaries(tournamentId: Int): Flow<List<PlayerScoreSummary>>

    @Query("""
        SELECT p.id AS playerId, p.first_name AS firstName, p.last_name AS lastName,
               SUM(s.strokes) AS totalStrokes, SUM(s.strokes - s.par) AS totalVsPar,
               COUNT(s.id) AS holesPlayed
        FROM players p
        INNER JOIN scores s ON s.player_id = p.id
        WHERE s.tournament_id = :tournamentId
        GROUP BY p.id
        ORDER BY totalStrokes ASC
        LIMIT :limit
    """)
    fun getLeaderboard(tournamentId: Int, limit: Int = 10): Flow<List<LeaderboardEntry>>

    @Transaction
    suspend fun replaceScoresForPlayer(playerId: Int, tournamentId: Int, scores: List<Score>) {
        deleteScoresForPlayer(playerId, tournamentId)
        insertScores(scores)
    }
}
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Live: Seed Data for Testing

// In the ViewModel or a TestDataSeeder, called once:
fun seedTestData() {
    viewModelScope.launch {
        val tournamentId = repository.insertTournament(
            Tournament(name = "Spring Classic", date = "2026-04-05", location = "Pebble Beach")
        ).toInt()

        val playerId = repository.insertPlayer(
            Player(firstName = "Alice", lastName = "Chen",
                   handicap = 8.2, tournamentId = tournamentId)
        ).toInt()

        repository.insertScores(
            (1..18).map { hole ->
                Score(playerId = playerId, tournamentId = tournamentId,
                      hole = hole, par = if (hole % 3 == 0) 5 else 4,
                      strokes = (3..6).random())
            }
        )
    }
}
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Verifying in Database Inspector

-- After seeding — verify in Inspector query console:

-- 1. Check tournament was inserted:
SELECT * FROM tournaments;

-- 2. Check player count for the tournament:
SELECT COUNT(*) FROM players WHERE tournament_id = 1;

-- 3. Verify all 18 holes were inserted:
SELECT hole, par, strokes, (strokes - par) AS vs_par
FROM scores
WHERE player_id = 1 AND tournament_id = 1
ORDER BY hole ASC;

-- 4. Run the leaderboard query directly:
SELECT p.first_name, p.last_name, SUM(s.strokes) AS total, SUM(s.strokes - s.par) AS vs_par
FROM players p INNER JOIN scores s ON s.player_id = p.id
WHERE s.tournament_id = 1 GROUP BY p.id ORDER BY total ASC;
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Common Room Pitfalls

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Pitfall 1: Missing @Transaction on @Relation

// ❌ Missing @Transaction — data can be inconsistent
@Query("SELECT * FROM tournaments WHERE id = :id")
fun getTournamentWithPlayers(id: Int): Flow<TournamentWithPlayers?>

// ✅ Always add @Transaction with @Relation
@Transaction
@Query("SELECT * FROM tournaments WHERE id = :id")
fun getTournamentWithPlayers(id: Int): Flow<TournamentWithPlayers?>

Room warns about this but doesn't fail the build. Always add it.

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Pitfall 2: Calling suspend on Main Thread

// ❌ Direct call in an onClick — crashes with IllegalStateException
Button(onClick = {
    repository.insertTournament(tournament)   // suspend — can't call here
}) { Text("Save") }

// ✅ Always go through ViewModel / viewModelScope
Button(onClick = {
    viewModel.saveTournament(tournament)      // regular function
}) { Text("Save") }

// In ViewModel:
fun saveTournament(tournament: Tournament) {
    viewModelScope.launch { repository.insertTournament(tournament) }
}
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Pitfall 3: Forgetting to Increment Version

// You added a column to Player — now you must:

// 1. Bump the version number:
@Database(entities = [...], version = 2)   // was 1

// 2. Provide a migration:
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 ''")
    }
}

// 3. Register the migration:
Room.databaseBuilder(...).addMigrations(MIGRATION_1_2).build()

Forgetting version → IllegalStateException: Room cannot verify the data integrity on upgrade.

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Pitfall 4: Collecting Flow Outside a Scope

// ❌ Leaks — no lifecycle awareness, collects forever
val scores = scoreDao.getScoresForPlayer(playerId)
scores.collect { ... }   // called outside a coroutine scope

// ✅ Collect in ViewModel with stateIn() — lifecycle-aware
val scores: StateFlow<List<Score>> = scoreDao.getScoresForPlayer(playerId)
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = emptyList()
    )

Always collect Room Flows in a CoroutineScope with a defined lifecycle.

Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Summary

Advanced Queries

  • Filtering: WHERE :param
  • Aggregation: SUM, AVG, COUNT
  • Joins: INNER JOIN ... ON
  • Group + sort: GROUP BY ... ORDER BY
  • Custom result types: match AS aliases

Relationships

  • @Embedded — flatten into one table
  • @Relation — one-to-many, always @Transaction

Debugging

  • Database Inspector: View → App Inspection
  • Browse tables live, run SQL queries
  • Verify migrations, check foreign keys
Week 12 Wednesday: Room Queries, Relationships & Database Inspector
CS 3180 Mobile Application Development

Week 12 Checklist

Task Notes
Lab 8: Room CodeLab Guided — follow each step carefully
Project: Add Room to your app All core data should go through Room
Quiz 10 Covers entities, DAOs, migrations, Flow queries
zyBook Ch 10 Sections 10.1–10.6 due Sunday

Next Week: Networking — Retrofit, REST APIs, and connecting Room to remote data.

Week 12 Wednesday: Room Queries, Relationships & Database Inspector

SPEAKER NOTES: Monday established the foundation: Entity, DAO, Database, Repository. Today we go deeper. Real apps need more than simple SELECT * queries — they need filtering, aggregation, relationships between tables, and tools to debug when things go wrong. All of that is on the agenda today.

SPEAKER NOTES: Simple queries don't scale. Fetching all scores and computing in Kotlin is fine with 10 rows, catastrophic with 17,000. The answer: push the computation into SQL, where the database engine can do it efficiently with indexes. Today we learn how to write those queries in Room.

SPEAKER NOTES: Frame the session. Monday's content covered 90% of what you need for the lab. Today covers the remaining 10% — but that 10% is what separates a demo app from a real app. Aggregation queries, relationships, and Database Inspector are all directly applicable to the team project.

SPEAKER NOTES: Room passes your @Query string directly to SQLite. If it's valid SQL, Room will execute it. That means every SQL feature you can use in SQLite is available: WHERE, ORDER BY, GROUP BY, HAVING, JOIN, subqueries, aggregate functions.

SPEAKER NOTES: Three important patterns. First: standard parameter binding with :paramName. Second: IN clause with a List — Room expands the list into the IN (...) syntax automatically. Third: LIKE search — the || operator is SQLite's string concatenation. The '%' || :query || '%' pattern wraps the search term in wildcards for a contains search. All of these are compile-time verified by Room.

SPEAKER NOTES: SQL aggregate functions work exactly as you'd expect. SUM and AVG return nullable types — they return NULL for zero matching rows in SQL, which Room maps to Kotlin nullable. COUNT always returns an integer (0 for no rows). These queries run inside SQLite's engine — efficient even over thousands of rows because SQLite can use indexes to avoid reading every row.

SPEAKER NOTES: This is a real leaderboard query: JOIN players to scores, GROUP BY player, SUM the strokes, ORDER BY lowest strokes, LIMIT to top N. This runs entirely in SQLite — no Kotlin processing of 17,000 rows. The return type is LeaderboardEntry — a data class we'll define next that maps to these specific column aliases (AS playerId, AS firstName, etc.).

SPEAKER NOTES: Room is flexible about return types. @Query doesn't have to return an entity — it can map to any data class whose property names match the column aliases. The AS clause in the SQL creates the alias. Room verifies the column-to-property mapping at compile time — if the alias doesn't match the property name, the build fails. This is more powerful than raw Cursor mapping and more efficient than loading full entities just to display a summary.

SPEAKER NOTES: A transaction groups multiple database operations into an atomic unit — either all succeed or none do. Room makes this easy with the @Transaction annotation.

SPEAKER NOTES: Atomicity matters for data integrity. If the process is killed between inserting the player and inserting the scores, you'd have scores in the database with a player_id that doesn't exist — a foreign key violation. With @Transaction, if anything fails after the first insert, the entire transaction rolls back to the state before the function ran. No half-writes. @Transaction can be placed on any DAO function or on a non-DAO suspend function in the Repository.

SPEAKER NOTES: A common sync pattern: you get fresh data from the server, want to replace everything in the local cache atomically. Without a transaction, a user might see an empty list between the delete and the re-insert. With @Transaction, the UI never observes the empty state — the delete and insert are invisible to collectors until both complete. This is how Room handles the "replace on sync" pattern.

SPEAKER NOTES: Relational databases have — relationships. SQLite supports foreign keys; Room provides two mechanisms to work with related data in Kotlin: @Embedded for simple nesting, and @Relation for proper one-to-many associations.

SPEAKER NOTES: @Embedded is not a join — it's column flattening. The Address fields (street, city, state) become columns in the players table directly. No second table, no join needed. Good for value objects that naturally belong to a single entity. Not good for shared or reusable data — if two players live at the same address, they each store a copy.

SPEAKER NOTES: @Relation is the real many-to-one join tool. TournamentWithPlayers is not an entity — it's a POJO that Room populates by running two queries: one for the tournament row, one for all players where tournament_id matches. @Transaction is required on @Relation queries — without it, Room runs the two queries separately and there's a window where the data could be inconsistent. Always pair @Relation with @Transaction.

SPEAKER NOTES: This gives you all players for a tournament, each with their complete list of Score objects attached. Room runs one query for the players list, then one batch query for all scores belonging to those players — it does NOT run N+1 queries (one per player). This is efficient. The result is a List<PlayerScoreSummary> you can render directly without any in-memory joining.

SPEAKER NOTES: Database Inspector is an Android Studio tool that lets you view, query, and modify your Room database while your app is running. It's invaluable for debugging schema issues, verifying that data was written correctly, and testing queries interactively.

SPEAKER NOTES: Live demo this. Open the golf app on the emulator, then show Database Inspector. Students are often surprised that they can see the database contents in real time. Emphasize: this works on an attached device too, not just the emulator. The database must be open (app running and has used the database at least once) for Inspector to connect.

SPEAKER NOTES: Click through the tables live. Insert a tournament from the app UI and watch the row appear in Inspector in real time. This is what "reactive" means — the database is live. Inspector also lets you click individual cells and edit values directly — useful for testing edge cases without writing UI for them. Show a live edit: change a player's name directly in Inspector, then observe the UI update via the Flow.

SPEAKER NOTES: Run these live in Inspector. The query console is powerful for debugging: you can prototype the exact SQL before writing it in a @Query annotation. If the SQL works in Inspector, it'll work in Room. This is also how to debug migration issues — run the query that the migration was supposed to enable and verify the schema is correct.

SPEAKER NOTES: The "Keep database connections open" toggle is easy to miss. Without it, Inspector periodically disconnects as Room manages connection lifecycle — frustrating when you're watching data change. With it enabled, Inspector stays connected as long as the app is running. Use Inspector to verify that: inserts produce the row you expect, updates target the right row, deletes cascade correctly via ForeignKey.CASCADE, and nullable columns contain the right null vs non-null values.

SPEAKER NOTES: Let's build the complete Room layer for the Hope Foundation scoring app. We'll go from empty project to a working database with transactions and live debugging in Inspector.

SPEAKER NOTES: Draw this on the board. Three entities, three DAOs, two ViewModels. This is the full database layer for the golf app — everything the project team might need. We won't build every piece live (too much), but we'll hit the most important: the Score DAO and leaderboard query, because those involve real SQL craft.

SPEAKER NOTES: Type this live. Point to each element: the @Relation query uses @Transaction (always), the leaderboard is the full JOIN/GROUP BY/ORDER BY query from earlier, and replaceScoresForPlayer is an atomic score replacement for when a scorer needs to correct an entry. After typing, run the app and open Database Inspector to verify the queries work.

SPEAKER NOTES: Seed data is essential for development — you can't test the leaderboard query without scores in the database. This seeds one tournament, one player, and 18 scores with random strokes. Add a "Seed" button in your debug UI or call this from a ViewModel init block guarded by BuildConfig.DEBUG. After seeding, open Database Inspector and verify all three tables have the expected rows. Then run the leaderboard query in the query console.

SPEAKER NOTES: Run each of these in Inspector live. Step through them in order: first verify the tournament exists (basic sanity), then the player count, then the individual scores, then the full leaderboard query. This is the debugging workflow students should adopt: write data, inspect it, verify your queries are correct before integrating into the ViewModel.

SPEAKER NOTES: Before closing, let's cover the mistakes that trip up students most often. These will save hours of debugging.

SPEAKER NOTES: Room issues a compile warning if you use @Relation without @Transaction. The warning is easy to miss and hard to reproduce as a bug — the inconsistency only occurs in a narrow race window. Add @Transaction as a rule: every DAO function that uses @Relation, always.

SPEAKER NOTES: Students often try to call suspend functions directly from composables. onClick lambdas run on the main thread — suspend functions called on the main thread without a coroutine scope throw IllegalStateException. The fix: always call through the ViewModel, which wraps the suspend call in viewModelScope.launch. The composable's onClick calls a regular ViewModel function, not a suspend function.

SPEAKER NOTES: This is the most common Room crash in student projects. During development, you can use fallbackToDestructiveMigration() to just drop and recreate the database — fine when there's no real user data. But in production, you must provide a real migration. The crash message "Room cannot verify the data integrity" means the expected schema doesn't match the actual schema — the version wasn't bumped or the migration wasn't registered.

SPEAKER NOTES: Flow.collect() is a suspend function — it can only be called from a coroutine. If you call it outside a scope, it'll block. If you call it in a CoroutineScope that outlives the screen, it'll leak. stateIn(viewModelScope) is the correct pattern: the collection is scoped to the ViewModel lifecycle, so it stops when the ViewModel is cleared. WhileSubscribed(5_000) adds an extra optimization: collection pauses when no UI is collecting, saving resources in the background.

SPEAKER NOTES: Two-column summary captures the two halves of today's session. The left column covers the SQL craft — these are skills that transfer to any relational database. The right column covers Room-specific patterns and tooling. Database Inspector alone is worth remembering: every time you're unsure what Room is actually writing to disk, open Inspector and look.

SPEAKER NOTES: The Lab 8 CodeLab is the priority this week — it's a structured walk through everything from Monday and today, with a pre-built project and step-by-step instructions. The project integration is the most valuable: take what you built in Lab 8 and apply it to your actual team project. Next week we add the network layer — fetch data from a server and cache it in Room. That's the full offline-first architecture.