A DAO function's return type decides how it runs. A suspend function runs once; a Flow function emits the result and runs again whenever a table it reads changes. Room 3.0 234 accepts nothing else, so a DAO can never block the main thread:
@Dao
interface BookDao {
...
@Query("SELECT * FROM books ORDER BY title")
suspend fun getAll(): List<BookEntity> // one-shot read
@Query("SELECT * FROM books ORDER BY title")
fun observeAll(): Flow<List<BookEntity>> // re-emits after every change
@Upsert
suspend fun upsertAll(books: List<BookEntity>) // insert or replace by id
@Insert
suspend fun addNote(note: NoteEntity): Long // returns the new rowid
@Transaction
@Query("SELECT * FROM books WHERE id IN (SELECT bookId FROM notes) ORDER BY title")
fun observeBooksWithNotes(): Flow<List<BookWithNotes>>
}The SQL is checked while the app builds: misspell a column and KSP stops with an error naming it. Parameters bind with :name (WHERE title LIKE '%' || :query || '%'), never pasted into the SQL, and @Upsert inserts or updates by primary key, the right operation for refreshing a cache.
BookNest's reading-notes screen collects observeBooksWithNotes() in a ViewModel. Adding a note updates it without any re-query by hand:
D/BookNestDB: notes flow: 1 books, 1 notes D/BookNestDB: inserted note 2 for book 3 D/BookNestDB: notes flow: 1 books, 2 notes
Room's invalidation tracker saw a write to notes and re-ran every live query that reads it. Use Flow for what a screen displays and suspend for one-off work, and keep Flow queries narrow: they re-run on any change to their tables.