B03 — Tiny notes
A notes app with a database: Room, a repository, and a ViewModel. The smallest project that shows why the data layer is separate, and what happens to your UI when the process is killed.
An app that stores data has to answer three questions: where the data lives, who owns it, and what the UI does while it loads. Room, a repository and a ViewModel are the conventional answers, and building the smallest version of each makes the conventions legible.
What you are building#
A list of notes with an add button, backed by a Room database. Each note has an id, a title and a body. The list updates automatically when the database changes, and a note survives process death.
Steps#
- Add the dependencies
Add Room, the Room compiler via KSP, and the lifecycle ViewModel library. Use a version catalog if your project has one; otherwise declare versions once in the module's build file.1
You should seeThe project still builds. - Define the entity
Create a
Noteentity with an auto-generated id, a title and a body. Annotate the class with@Entityand the id with@PrimaryKey(autoGenerate = true).You should seeA data class with a primary key. - Define the DAO
Create a
NoteDaointerface with a query returning aFlow<List<Note>>and an insert function markedsuspend. TheFlowis what makes the list live: the query re-emits when the table changes.You should seeCompiles, with generated code. - Create the database
Build a
RoomDatabasewithRoom.databaseBuilder, register the entity and expose the DAO. Keep the instance in a single place; a second instance is a second connection and a source of confusion.You should seeA database instance that opens. - Add a repository
Wrap the DAO in a repository that exposes the flow and the insert. It looks like ceremony for one table; it is the seam where a cache or a network source would go, and it keeps the ViewModel from knowing about Room.
You should seeThe ViewModel has one dependency. - Add the ViewModel
Expose the notes as a
StateFlowderived from the repository's flow, withstateInand aviewModelScope. The UI collects it; the ViewModel survives a rotation.You should seeA state flow of notes. - Build the UI
Compose a
LazyColumnover the notes, with a button that opens a simple form and inserts. Collect the state withcollectAsStateWithLifecycleso collection stops when the app is not visible.You should seeA list that updates when you add a note. - Kill the process and reopen
Use
adb shell am killon the package while the app is backgrounded, then reopen. The database persists, the ViewModel does not, and the data survives — which is the whole point of the data layer.You should seeThe notes are still there.
The entity is a plain data class with annotations:
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val body: String,
)The DAO exposes a reactive query and a suspend insert:
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY id DESC")
fun observeAll(): Flow<List<Note>>
@Insert
suspend fun insert(note: Note): Long
}adb shell run-as com.example.tinynotes ls databases
adb shell run-as com.example.tinynotes ls -l databases
adb shell am kill com.example.tinynotesExample output
Illustrative — the schema and path depend on your build.Verification#
Add a note, rotate the device, kill the process, reopen. The note should be present after all three.
Then change the query to return a plain list instead of a Flow and observe that the UI no longer
updates without a manual refresh — that is the difference the reactive query buys you.
What you learned#
- The database is the source of truth, not the UI state. The UI observes it.
- A repository is a seam, not ceremony: it is where a second data source would be added.
- The ViewModel survives configuration changes but not process death; the database survives both.
- Reactive queries remove a class of bugs, because there is no separate refresh path to forget.
Read App data and databases for the storage model, and Data and domain layers for where the repository fits in a larger app.
Extend it#
- Add a search query that filters with a
WHERE title LIKE :term, and observe that the flow re-emits on both edits and new inserts. - Add a second table with a relation, and use
@Relationto load a note with its tags. - Add an export that writes the notes to a file in the app's external files directory, then pull
it with
adb pull.
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsGuide to app architecturedeveloper.android.comaccessed 2026-10-27↩
- docsSave data in a local database using Room | App data and filesdeveloper.android.comaccessed 2026-10-27↩
- docsData and file storage overview | App data and filesdeveloper.android.comaccessed 2026-10-27↩
- docsKotlin coroutines on Androiddeveloper.android.comaccessed 2026-10-27↩
- docsJetpackdeveloper.android.comaccessed 2026-10-27↩