Build a Book Tracker App with Room
Build an Android app that stores your reading list in a local Room database.
Introduction
30 Second Summary
You finish a great book and think "I should remember this." A week later, you can't recall the title. Keeping a reading list shouldn't require sticky notes or a spreadsheet you'll never open again.
In this project, you will build a Book Tracker app for Android that stores your reading list permanently on your device using a local database. You'll add books, mark them as read, delete ones you don't want, and see everything survive app restarts.
What You'll Build
Open the app on your phone emulator, see your full reading list, tap to add a new book, check it off when you finish it, and close the app knowing it will all be there when you come back.
By the end of this project, you'll have:
- A working Android app where you can add, read, update, and delete books from a scrollable list with instant visual feedback on every action.
- A persistent Room database that saves your reading list permanently on the device, surviving app restarts with zero internet required.
- A clean codebase using the MVVM architecture pattern with Jetpack Compose UI that mirrors how production Android apps are structured.
- Secret Mission: add a real-time search bar that filters your books by title or author as you type.
Are there any prerequisites?
None. This guide walks you through installing Android Studio, setting up the emulator, and creating your project from scratch. No prior Android experience required.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Project and Emulator
Before you can build a book tracker, you need two things: a project to hold your code and a virtual phone to test it on.
In this step, you will create a new Android Studio project using the Empty Activity template. Then you will configure the Android Emulator so you have a virtual device to run your app on.
In this step, get ready to:
- Create a new BookTracker project with Jetpack Compose.
- Set up a Pixel 8 virtual device in the Android Emulator.
- Run the app and see "Hello Android!" on screen.
Create a new project
The Empty Activity template gives you a ready-made Jetpack Compose project with Material 3 included. You just need to name it and pick an API level.
- In Android Studio, click File in the top menu bar.
- Click New, then click New Project.
- In the Templates pane, make sure Phone and Tablet is selected on the left.
- Select Empty Activity, then click Next.
- In the Name field, type BookTracker.
- Confirm the Package name field shows com.example.booktracker.
- Set Minimum SDK to API 24.
- Click Finish.
Android Studio will generate the project files and run a Gradle sync. Wait until the progress bar at the bottom finishes.
What just happened?
Android Studio created a complete Compose project for you. It includes a MainActivity.kt file that displays a greeting on screen, plus all the Gradle configuration needed to build and run the app.
Gradle sync taking a long time?
The first sync downloads dependencies from the internet. This can take a few minutes on a slower connection. Check the bottom status bar for progress.
If sync fails, make sure you are connected to the internet and try clicking File then Sync Project with Gradle Files.
help me fix a Gradle sync error in Android Studio
Set up the Android Emulator
You need a virtual phone to run your app on. The Device Manager lets you create one that simulates a real Pixel device on your computer.
- Click Tools in the top menu bar.
- Click Device Manager.
- In the Device Manager panel, click the + button, then select Create Virtual Device.
- In the Phone category tab, select Pixel 8.
- Click Next.
- On the System Image screen, find the API 34 system image. If you see a Download link next to it, click it to download the image first.
- Select the API 34 system image, then click Next.
- On the Verify Configuration screen, leave the defaults and click Finish.
Your new Pixel 8 virtual device now appears in the Device Manager panel.
What is an AVD?
AVD stands for Android Virtual Device. It is a software-based phone that runs on your computer. It behaves just like a real phone, complete with its own storage and Android operating system.
System image download is slow?
System images are large files (1-2 GB). The download can take several minutes depending on your internet speed. Let it finish completely before clicking Next.
If the download fails, close the dialog, reopen Device Manager, and try again.
help me download a system image for the Android Emulator
Run the app on the emulator
With the project created and the virtual device ready, you can launch the app.
- In the toolbar at the top of Android Studio, find the target device dropdown menu.
- Select your Pixel 8 virtual device from the dropdown.
Before you hit Run, what do you think you will see on the emulator screen?
- Click the green Run button in the toolbar (or press Shift+F10).
First launch takes a while
The emulator needs to boot the entire Android operating system the first time it starts. This can take 1-3 minutes. Subsequent launches are much faster because Android Studio takes a snapshot.
Once the emulator finishes booting, you should see "Hello Android!" displayed on the virtual phone screen.
Don't see the app on the emulator?
Make sure you selected the Pixel 8 virtual device in the target device dropdown before clicking Run.
If the emulator booted but shows a home screen without your app, wait a moment. The app may still be installing. Check the Run tab at the bottom of Android Studio for progress or error messages.
help me get my app running on the Android Emulator
Your project is set up and your emulator is running. Next up, you will build a scrollable book list with real UI components.
Build a Book List with Fake Data
Your project is set up and the Emulator is showing your app. Now it's time to build the screen your users will actually see.
Before wiring up a database, you want to see what the finished UI looks like with your own eyes. In this step, you'll build a scrollable book list using hardcoded sample data so you can focus purely on the layout and styling.
In this step, get ready to:
- Define a Book data class to represent each book.
- Build a scrollable list screen using LazyColumn and hardcoded sample books.
- Style each book entry with Material 3 Card components.
Define the Book data class
Every book in your list needs a consistent shape: a title, an author, and a flag for whether you've read it. A data class in Kotlin is the perfect container for this kind of structured data.
- In the Project panel on the left side of Android Studio, expand app > java > com.example.booktracker.
- Right-click the com.example.booktracker package folder.
- Select New then Kotlin Class/File.
- Type Book as the name, select File from the list, and press Enter.
Android Studio creates Book.kt and opens it in the editor. Replace any generated content with this code:
package com.example.booktracker
data class Book(
val id: Int = 0,
val title: String,
val author: String,
val isRead: Boolean = false
)
What does this code do?
- data class Book creates a Kotlin data class that automatically provides equals(), toString(), and copy() for free.
- id uniquely identifies each book. The default value of 0 means you can create a book without specifying an ID.
- isRead defaults to false because new books start as unread.
- Save Book.kt with Ctrl+S.
- Confirm the file appears under com.example.booktracker in the Project panel, alongside MainActivity.kt.
Don't see Book.kt in the project panel?
- Make sure the Project panel dropdown at the top is set to Android view (not Project Files).
- Confirm you right-clicked on com.example.booktracker inside the java folder, not a different package.
Why isn't my new Kotlin file showing up in the project?
✔️ Awesome, I've got everything!
Great. Make sure Book.kt is saved before moving on.
ⓧ I'd like to double check the full code
package com.example.booktracker
data class Book(
val id: Int = 0,
val title: String,
val author: String,
val isRead: Boolean = false
)
Build the screen structure in MainActivity
Now you'll replace the placeholder text in MainActivity.kt with the scaffolding for your book list. This includes the sample data, the updated MainActivity class, and the list screen composable.
- Open MainActivity.kt in the editor by clicking it in the Project panel.
- Select all the existing code with Ctrl+A and delete it.
- Paste in the following package declaration and imports:
package com.example.booktracker
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.booktracker.ui.theme.BookTrackerTheme
What are all these imports?
Each import brings in a specific building block from Jetpack Compose and Material 3. The layout imports control how elements are arranged. The material3 imports provide ready-made UI components like cards and checkboxes.
You won't need to memorize these. Android Studio can auto-import them as you type, but having them upfront avoids red underlines while you build.
- Directly below the imports, add the sample book data and the updated MainActivity class:
val sampleBooks = listOf(
Book(1, "The Hobbit", "J.R.R. Tolkien"),
Book(2, "1984", "George Orwell"),
Book(3, "Dune", "Frank Herbert"),
Book(4, "Brave New World", "Aldous Huxley")
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BookTrackerTheme {
BookListScreen(books = sampleBooks, onToggleRead = {})
}
}
}
}
What does this code do?
- sampleBooks creates four hardcoded books for testing. Each uses the Book data class you just created.
- BookListScreen(books = sampleBooks, onToggleRead = {}) replaces the old Text("Hello Android!"). It passes the sample books and an empty lambda for the toggle action (you'll wire that up later).
Android Studio will show BookListScreen highlighted in red because it doesn't exist yet. That's expected. You'll define it next.
- Below the MainActivity class, add the BookListScreen composable:
@Composable
fun BookListScreen(
books: List<Book>,
onToggleRead: (Book) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(books, key = { it.id }) { book ->
BookItem(
book = book,
onToggleRead = { onToggleRead(book) }
)
}
}
}
What does BookListScreen do?
- LazyColumn is Compose's scrollable list. It only renders items visible on screen, keeping performance smooth even with hundreds of books.
- items(books, key = { it.id }) tells Compose to create one BookItem for each book, using the ID as a stable key so animations work correctly.
- Arrangement.spacedBy(8.dp) adds 8dp of vertical spacing between each book card.
Add the BookItem composable
Each book in the list needs its own visual component showing the title, author, and a read/unread checkbox. The BookItem composable wraps this in a Card for clean Material 3 styling.
- Below BookListScreen, add the BookItem composable:
@Composable
fun BookItem(
book: Book,
onToggleRead: () -> Unit
) {
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = book.isRead,
onCheckedChange = { onToggleRead() }
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
) {
Text(text = book.title, style = MaterialTheme.typography.titleMedium)
Text(text = book.author, style = MaterialTheme.typography.bodyMedium)
}
}
}
}
What does BookItem do?
- Card wraps each book in a Material 3 elevated surface with rounded corners and a subtle shadow.
- Row lays out the checkbox and text side by side. Alignment.CenterVertically keeps them aligned at their midpoint.
- Checkbox shows the book's read status. Tapping it calls onToggleRead(), which is an empty action for now.
- Column stacks the title and author vertically. .weight(1f) makes it fill the remaining horizontal space after the checkbox.
- Save MainActivity.kt with Ctrl+S.
Before you run the app, what do you expect to see on the Emulator?
- Click the green Run button (or press Shift+F10) to build and launch the app on the Emulator.
You should see a scrollable list of four books. Each book appears in its own card with a checkbox on the left, the title in bold, and the author below it.
Seeing a build error instead of the book list?
- Check the Build panel at the bottom of Android Studio for the specific error message.
- If you see "Unresolved reference: Book", confirm that Book.kt is in the same package (com.example.booktracker) as MainActivity.kt.
- If you see "Unresolved reference: BookItem" or "Unresolved reference: BookListScreen", make sure both composable functions are pasted below the MainActivity class in the same file.
My app won't build after adding the book list code.
✔️ Awesome, I've got everything!
Great. Confirm you have saved both Book.kt and MainActivity.kt before moving on.
ⓧ I'd like to double check the full code
Since MainActivity.kt is a larger file, scroll up and confirm each section is present in this order:
- Package declaration and all imports (starting with import android.os.Bundle through import com.example.booktracker.ui.theme.BookTrackerTheme).
- The sampleBooks list with 4 books (The Hobbit, 1984, Dune, Brave New World).
- The MainActivity class calling BookListScreen(books = sampleBooks, onToggleRead = {}).
- The BookListScreen composable with a LazyColumn.
- The BookItem composable with a Card, Row, Checkbox, and Column.
For Book.kt, confirm it matches:
package com.example.booktracker
data class Book(
val id: Int = 0,
val title: String,
val author: String,
val isRead: Boolean = false
)
You now have a working book list screen built entirely with Jetpack Compose. Next up, you'll make this data permanent so it survives when you close and reopen the app.
Add Room Database for Permanent Storage
Your book list from the last step looks great, but it has a fatal flaw. Close the app and reopen it, and you get the exact same hardcoded data every time.
Room is Android's official database library. It stores your data in an on-device SQLite database that survives app restarts, crashes, and even device reboots. In this step, you will wire up Room so your books persist permanently.
In this step, get ready to:
- Add Room dependencies to your project's Gradle files.
- Convert your Book data class into a Room Entity and create the database classes.
- Wire the database into MainActivity so the book list displays live data from Room.
Add Room dependencies to your project
Room relies on a code generation tool called KSP (Kotlin Symbol Processing) to turn your annotations into real database code at compile time. You need to declare the KSP plugin and then add three Room libraries.
- In Android Studio, open the Project panel on the left side.
- Expand the project root and open the top-level build.gradle.kts file (the one at the very top of the project, NOT inside the app folder).
- Find the plugins { } block near the top of the file.
- Add the KSP plugin declaration inside that block by pasting this line:
id("com.google.devtools.ksp") version "2.3.4" apply false
What does this line do?
This registers the KSP plugin at the project level without applying it yet. The apply false part means individual modules (like app) can opt in to using it separately.
- Now open the app-level build.gradle.kts file (inside the app folder).
- Find the plugins { } block at the top of this file.
- Add this line inside it to apply KSP to your app module:
id("com.google.devtools.ksp")
- Scroll down to the dependencies { } block in the same file.
- Add these three Room dependencies inside the block:
// Room database
implementation("androidx.room:room-runtime:2.8.4")
implementation("androidx.room:room-ktx:2.8.4")
ksp("androidx.room:room-compiler:2.8.4")
What are these three dependencies?
room-runtime is the core Room library that handles database operations. room-ktx adds Kotlin coroutine support so you can run database operations off the main thread.
room-compiler uses KSP to generate the implementation code for your DAO interfaces at compile time. That is why it uses ksp() instead of implementation().
- Click the Sync Now banner that appears at the top of the editor to sync your Gradle files.
You should see "BUILD SUCCESSFUL" in the Build output window at the bottom. This confirms Room and KSP are properly configured.
Gradle sync failed?
- Make sure you added the KSP plugin to BOTH Gradle files. The top-level file declares it, and the app-level file applies it.
- Check that the version string is exactly 2.3.4 for the KSP plugin and 2.8.4 for all three Room dependencies.
- Confirm ksp("androidx.room:room-compiler:2.8.4") uses ksp() and not implementation().
Gradle sync issues with Room dependencies
Convert Book to a Room Entity and create the database classes
Room needs three components to work: an Entity (your data table), a DAO (your query methods), and a Database class (the access point). You already have the Book data class, so you will start by converting it into a Room Entity.
- Open Book.kt in the editor.
- Replace the entire file contents with the following code:
package com.example.booktracker
import androidx.room.Entity
import androidx.room.PrimaryKey
// Room uses this class as the "books" table schema
@Entity(tableName = "books")
data class Book(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val author: String,
val isRead: Boolean = false
)
What changed here?
- @Entity(tableName = "books") tells Room to create a database table called "books" using this class's fields as columns.
- @PrimaryKey(autoGenerate = true) marks id as the unique identifier for each row and lets Room auto-increment it.
- Save Book.kt.
- Now create the DAO. Right-click the com.example.booktracker package in the Project panel.
- Select New, then Kotlin Class/File.
- Choose Interface and name it BookDao.
- Replace the generated file contents with this code:
package com.example.booktracker
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface BookDao {
// Returns a live-updating list sorted alphabetically
@Query("SELECT * FROM books ORDER BY title ASC")
fun getAllBooks(): Flow<List<Book>>
@Insert
suspend fun insertBook(book: Book)
@Update
suspend fun updateBook(book: Book)
@Delete
suspend fun deleteBook(book: Book)
}
What does the DAO do?
A DAO (Data Access Object) defines the methods your app uses to interact with the database. You write the method signatures and Room generates the implementation at compile time.
- getAllBooks() returns a Flow that automatically emits a new list whenever the books table changes.
- The suspend keyword on write operations (insert, update, delete) means they run off the main thread so your UI stays smooth.
- Save BookDao.kt.
- Build the project by clicking Build in the top menu, then Make Project (or press Ctrl+F9).
You should see "BUILD SUCCESSFUL" in the output. This confirms Room successfully processed your Entity and DAO annotations.
Build failed with annotation processing errors?
- Make sure @Entity(tableName = "books") is above the data class Book line, not inside it.
- Check that BookDao is an interface (not a class). Room generates the implementation for you.
- Verify the SQL string in @Query references the table name books exactly as you defined it in the @Entity annotation.
Help me fix my Room DAO build errors
- Create one more file. Right-click the com.example.booktracker package again.
- Select New, then Kotlin Class/File.
- Choose Class and name it BookDatabase.
- Replace the generated file contents with this code:
package com.example.booktracker
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
// Declares this as the Room database with the Book entity
@Database(entities = [Book::class], version = 1, exportSchema = false)
abstract class BookDatabase : RoomDatabase() {
abstract fun bookDao(): BookDao
companion object {
@Volatile
private var INSTANCE: BookDatabase? = null
// Singleton ensures only one database instance exists
fun getDatabase(context: Context): BookDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
BookDatabase::class.java,
"book_database"
).build()
INSTANCE = instance
instance
}
}
}
}
What does the Database class do?
The @Database annotation tells Room which entities (tables) belong to this database. The abstract bookDao() function gives your app access to the DAO.
The singleton pattern (companion object with @Volatile and synchronized) ensures only one database instance exists at a time. Multiple instances could corrupt your data.
- Save BookDatabase.kt.
Wire the database into MainActivity
You have all three Room components ready: Entity, DAO, and Database. Now you need to replace the hardcoded sampleBooks list in MainActivity.kt with live data from Room.
- Open MainActivity.kt in the editor.
- Find and delete the sampleBooks val at the top of the file (the entire val sampleBooks = listOf(...) block).
- In the import section at the top, remove these three lines that are no longer needed:
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
- Add these four new imports at the end of the import section:
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
- Now replace the entire MainActivity class with this updated version:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Build the Room database and get the DAO
val db = BookDatabase.getDatabase(this)
val dao = db.bookDao()
// Pre-populate with sample books on a background thread
CoroutineScope(Dispatchers.IO).launch {
dao.insertBook(Book(title = "The Hobbit", author = "J.R.R. Tolkien"))
dao.insertBook(Book(title = "1984", author = "George Orwell"))
dao.insertBook(Book(title = "Dune", author = "Frank Herbert"))
}
setContent {
BookTrackerTheme {
// Collect the Flow as Compose state so UI updates automatically
val books by dao.getAllBooks().collectAsState(initial = emptyList())
BookListScreen(books = books, onToggleRead = {})
}
}
}
}
What changed in MainActivity?
- The hardcoded sampleBooks list is gone. Instead, you build the database with BookDatabase.getDatabase(this) and get the DAO.
- A CoroutineScope(Dispatchers.IO) block inserts sample books on a background thread so the UI does not freeze.
- dao.getAllBooks().collectAsState(initial = emptyList()) turns the Room Flow into Compose state. Whenever the database changes, the UI automatically updates.
- Leave the BookListScreen and BookItem composables unchanged. They still work exactly as before.
- Save MainActivity.kt.
Before you run the app, what do you think will happen? The hardcoded list is gone, but you have wired in database inserts and a Flow collector. Will you see books or an empty screen?
- Click the green Run button (or press Shift+F10) to launch the app on your emulator.
You should see The Hobbit, 1984, and Dune in your book list. The list looks the same as before, but the data now comes from an actual database stored on the device.
Seeing a blank screen or a crash?
- Check the Logcat panel at the bottom of Android Studio for error messages. Look for "FATAL EXCEPTION" or Room-related errors.
- Make sure you removed the old sampleBooks val and the unused mutableStateOf and remember imports. Leftover code from the previous version causes compilation errors.
- Verify that BookDatabase.kt is in the same package (com.example.booktracker) as your other files.
Help me debug my Room database app crash
Now prove the data is actually in a real database by using the Database Inspector.
- In Android Studio (with the app still running on the emulator), click View in the top menu bar.
- Select Tool Windows, then App Inspection.
- In the App Inspection panel that opens at the bottom, select the Database Inspector tab.
- You should see book_database in the left pane. Click the books table to see your three books stored as database rows.
- Click Open New Query tab at the top of the Databases pane.
- Type the following SQL query into the text field and click Run:
INSERT INTO books (title, author, isRead) VALUES ('Neuromancer', 'William Gibson', 0)
Look at the app on your emulator. "Neuromancer" should appear in the list automatically without restarting the app. This proves that your Flow is reactive. Room detects the database change and pushes the updated list to your UI in real time.
Why did the list update without a refresh?
When your DAO returns a Flow<List<Book>>, Room automatically re-emits the query result whenever the underlying table changes. The collectAsState() call in Compose subscribes to that Flow, so any database change triggers a UI recomposition.
✔️ Awesome, I've got everything!
Great. Make sure all four files (Book.kt, BookDao.kt, BookDatabase.kt, MainActivity.kt) are saved.
ⓧ I'd like to double check the full code
Here are the three new or updated files in full. For MainActivity.kt, scroll up and confirm your file has the updated imports, the MainActivity class with database wiring, then the unchanged BookListScreen and BookItem composables at the bottom.
package com.example.booktracker
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "books")
data class Book(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val author: String,
val isRead: Boolean = false
)
package com.example.booktracker
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface BookDao {
@Query("SELECT * FROM books ORDER BY title ASC")
fun getAllBooks(): Flow<List<Book>>
@Insert
suspend fun insertBook(book: Book)
@Update
suspend fun updateBook(book: Book)
@Delete
suspend fun deleteBook(book: Book)
}
package com.example.booktracker
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Book::class], version = 1, exportSchema = false)
abstract class BookDatabase : RoomDatabase() {
abstract fun bookDao(): BookDao
companion object {
@Volatile
private var INSTANCE: BookDatabase? = null
fun getDatabase(context: Context): BookDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
BookDatabase::class.java,
"book_database"
).build()
INSTANCE = instance
instance
}
}
}
}
For MainActivity.kt, confirm it has these sections in order:
- The package declaration and imports (including collectAsState, CoroutineScope, Dispatchers, and launch).
- The MainActivity class that builds the database, pre-populates it, and collects the Flow.
- The unchanged BookListScreen composable.
- The unchanged BookItem composable.
Your book data now lives in a real database that survives app restarts. Next up, you will add buttons to create and delete books directly from the app's UI.
Add and Delete Books from the UI
Your book list is now backed by a real database. Every book you see on the Emulator is pulled from Room and survives app restarts.
There is one problem. You have no way to add new books or remove old ones from inside the app. The only way to modify data right now is through the Database Inspector. In this step, you will build the user-facing controls that make the app actually useful.
In this step, get ready to:
- Create a dialog for adding new books with title and author fields.
- Wire up a floating action button and make the read checkbox persist changes.
- Add a delete button to each book card.
Create the add book dialog
Before you can add books from the UI, you need a dialog that collects the book's title and author. Jetpack Compose provides an AlertDialog composable that handles this perfectly.
- Open MainActivity.kt in Android Studio.
- Add these new imports at the top of the file, below the existing imports:
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
Auto-import shortcut
If you miss an import, Android Studio underlines the unresolved symbol in red. Place your cursor on it and press Alt+Enter (Windows) or Option+Enter (macOS) to add the import automatically.
- Scroll to the bottom of MainActivity.kt and add the AddBookDialog composable below the existing BookItem function:
@Composable
fun AddBookDialog(onDismiss: () -> Unit, onConfirm: (String, String) -> Unit) {
var title by remember { mutableStateOf("") }
var author by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Add a Book") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
TextField(value = title, onValueChange = { title = it }, label = { Text("Title") }, modifier = Modifier.fillMaxWidth())
TextField(value = author, onValueChange = { author = it }, label = { Text("Author") }, modifier = Modifier.fillMaxWidth())
}
},
confirmButton = {
TextButton(onClick = { if (title.isNotBlank() && author.isNotBlank()) onConfirm(title, author) }) { Text("Save") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") }
}
)
}
What does this code do?
- Two remember { mutableStateOf("") } variables hold the text the user types into each field. Compose re-renders automatically as they change.
- The confirmButton only fires onConfirm when both fields are non-blank. This prevents saving empty books.
- The onDismiss callback lets the parent composable close the dialog without saving.
Add the floating action button and wire the checkbox
The dialog exists, but nothing opens it yet. You need a FloatingActionButton (the round "+" button common in Android apps) and you also need the checkbox tap to actually persist changes to Room.
- In MainActivity.kt, find the onCreate method inside class MainActivity.
- Replace the entire onCreate method with the following code:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val db = BookDatabase.getDatabase(this)
val dao = db.bookDao()
setContent {
BookTrackerTheme {
val books by dao.getAllBooks().collectAsState(initial = emptyList())
var showDialog by remember { mutableStateOf(false) }
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = { showDialog = true }) {
Icon(Icons.Default.Add, contentDescription = "Add Book")
}
}
) { innerPadding ->
BookListScreen(
books = books,
onToggleRead = { book ->
CoroutineScope(Dispatchers.IO).launch {
dao.updateBook(book.copy(isRead = !book.isRead))
}
},
modifier = Modifier.padding(innerPadding)
)
if (showDialog) {
AddBookDialog(
onDismiss = { showDialog = false },
onConfirm = { title, author ->
CoroutineScope(Dispatchers.IO).launch {
dao.insertBook(Book(title = title, author = author))
}
showDialog = false
}
)
}
}
}
}
}
What changed from the previous version?
- The hardcoded sample-data insertion is gone. Those books are already in the database from previous runs.
- A Scaffold now wraps the screen, providing the FloatingActionButton in the bottom-right corner.
- The onToggleRead lambda now calls dao.updateBook(book.copy(isRead = !book.isRead)) inside a coroutine, so tapping a checkbox actually persists the change to Room.
- When showDialog is true, the AddBookDialog appears. On confirm, it inserts the new book and flips the flag back to false.
- Save MainActivity.kt and click the green Run button (or press Shift+F10).
- On the Emulator, tap the round "+" button in the bottom-right corner.
- Type a title (e.g. Atomic Habits) and an author (e.g. James Clear), then tap Save.
You should see the new book appear instantly in your list. Tap its checkbox to mark it as read, then close and reopen the app to confirm the toggle persisted.
Don't see the FAB or dialog?
- Check that the Icons.Default.Add import resolved correctly. The red underline means the import is missing.
- Make sure you removed the old CoroutineScope(Dispatchers.IO).launch { dao.insertBook(...) } block that inserted sample data. Leaving it causes duplicate books on every launch.
- If the dialog appears but the book does not show up, confirm you have dao.insertBook(Book(title = title, author = author)) inside the onConfirm lambda.
Still stuck? Help me troubleshoot why my FloatingActionButton or AddBookDialog is not working.
Add a delete button to each book
You can add and read books, but you cannot remove them. A delete icon on each card gives users a quick way to clean up their list.
- In MainActivity.kt, find the BookListScreen composable and replace it with this updated version that accepts an onDelete callback:
@Composable
fun BookListScreen(
books: List<Book>,
onToggleRead: (Book) -> Unit,
onDelete: (Book) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(books, key = { it.id }) { book ->
BookItem(book = book, onToggleRead = { onToggleRead(book) }, onDelete = { onDelete(book) })
}
}
}
What changed?
The new onDelete: (Book) -> Unit parameter lets the parent pass a delete action down to each BookItem. The list now forwards both the toggle and delete callbacks per book.
- Find the BookItem composable below BookListScreen and replace it with this version that adds a delete icon:
@Composable
fun BookItem(book: Book, onToggleRead: () -> Unit, onDelete: () -> Unit) {
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = book.isRead, onCheckedChange = { onToggleRead() })
Column(modifier = Modifier.weight(1f).padding(start = 8.dp)) {
Text(text = book.title, style = MaterialTheme.typography.titleMedium)
Text(text = book.author, style = MaterialTheme.typography.bodyMedium)
}
// Delete button on the right side of each card
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "Delete")
}
}
}
}
What does the delete icon do?
The IconButton wraps a trash-can icon (Icons.Default.Delete). When tapped, it fires the onDelete callback, which ultimately calls dao.deleteBook(book) on a background thread.
- Now wire the delete action in onCreate. Find the BookListScreen call inside the Scaffold block. It currently looks like this:
BookListScreen(
books = books,
onToggleRead = { book ->
CoroutineScope(Dispatchers.IO).launch {
dao.updateBook(book.copy(isRead = !book.isRead))
}
},
modifier = Modifier.padding(innerPadding)
)
- Add an onDelete parameter between onToggleRead and modifier, so the call now looks like this:
BookListScreen(
books = books,
onToggleRead = { book ->
CoroutineScope(Dispatchers.IO).launch {
dao.updateBook(book.copy(isRead = !book.isRead))
}
},
onDelete = { book ->
CoroutineScope(Dispatchers.IO).launch { dao.deleteBook(book) }
},
modifier = Modifier.padding(innerPadding)
)
Before you run, predict: what do you think will happen when you tap the trash icon on a book?
- Save MainActivity.kt and run the app (Shift+F10).
- Tap the trash icon on any book in your list.
The book disappears instantly from the list. Close the app completely and reopen it. The deleted book stays gone because the delete was persisted to Room.
Seeing a compile error about onDelete?
- Make sure you updated both BookListScreen and BookItem to accept the onDelete parameter. If only one is updated, the function signatures will not match.
- Verify the import androidx.compose.material.icons.filled.Delete line is present at the top of the file. Without it, Icons.Default.Delete will not resolve.
Still stuck? Help me fix a compile error related to onDelete in my BookTracker composables.
✔️ Awesome, I've got everything!
Great. Double check you have saved MainActivity.kt and confirmed that adding, toggling, and deleting all persist across restarts.
ⓧ I'd like to double check the full code
Your MainActivity.kt is over 30 lines, so confirm these sections are present in order:
- All imports (including Icons, AlertDialog, FloatingActionButton, IconButton, TextField, mutableStateOf, remember, setValue).
- The onCreate method with Scaffold, FloatingActionButton, onToggleRead wired to dao.updateBook, onDelete wired to dao.deleteBook, and the AddBookDialog conditional.
- The BookListScreen composable with onToggleRead, onDelete, and modifier parameters.
- The BookItem composable with Checkbox, title/author text, and IconButton for delete.
- The AddBookDialog composable with two TextField inputs, Save button, and Cancel button.
Your app now supports full CRUD: you can create books, read the list, update the read status, and delete entries. Next up, you will introduce a ViewModel to separate this logic from the Activity and make it survive screen rotations.
Introduce a ViewModel for Clean Architecture
Your Book Tracker handles the full lifecycle of a book: add, mark as read, and delete. All of that data persists in Room. But there is a hidden fragility lurking in the code.
Right now, every database operation lives directly inside your Activity's onCreate. When Android rotates the screen, it destroys and recreates the entire Activity. That means orphaned background tasks, state that resets mid-interaction, and UI logic tangled with data operations. A ViewModel fixes all of this by surviving configuration changes and giving your data a stable home outside the Activity.
In this step, get ready to:
- Create a BookViewModel that manages database operations and exposes data as a StateFlow.
- Refactor MainActivity to use the ViewModel instead of calling the DAO directly.
- Verify the app survives screen rotation without data loss or crashes.
Before you build the fix, see the problem for yourself.
- Switch back to your running app on the Emulator.
- Tap the + button, type a title and author into the fields (don't tap Save yet).
Before you rotate, what do you think will happen to that half-filled dialog when the screen orientation changes?
- Rotate the Emulator by pressing Ctrl+Left (or Ctrl+Right) on your keyboard.
The dialog vanishes and your typed text is gone. Android destroyed and recreated the Activity, wiping all in-memory state. That is the gap a ViewModel closes.
Why does rotation destroy the Activity?
Android treats a screen rotation as a "configuration change." The system destroys the current Activity and creates a fresh one so it can reload resources (layouts, strings) for the new orientation. Any state stored in local variables or remember blocks is lost.
A ViewModel lives outside the Activity lifecycle. It survives destruction and hands its data back to the newly created Activity instantly.
Create the BookViewModel class
The ViewModel holds the database reference and exposes a reactive stream of books. It also provides action functions the UI calls without needing to know anything about Room or coroutines.
- Right-click the com.example.booktracker package in the Project panel.
- Select New, then Kotlin Class/File.
- Name it BookViewModel and choose Class.
- Replace the generated content with the following code:
package com.example.booktracker
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class BookViewModel(application: Application) : AndroidViewModel(application) {
// Get a reference to the DAO through the database singleton
private val dao = BookDatabase.getDatabase(application).bookDao()
// Convert the DAO's Flow into a StateFlow that survives rotation
val books: StateFlow<List<Book>> = dao.getAllBooks()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
What does this code do?
- AndroidViewModel gives the class access to the Application context (needed to build the database) and ties its lifecycle to the ViewModel's owner.
- dao.getAllBooks() returns a Flow that re-emits automatically whenever the books table changes.
- stateIn converts that cold Flow into a hot StateFlow scoped to viewModelScope. The last-emitted value is cached, so the UI never sees a blank flash on rotation.
Now add the action functions that the UI will call. Inside BookViewModel, paste the following below the books property (before the closing }):
// Add a new book to the database
fun addBook(title: String, author: String) {
viewModelScope.launch {
dao.insertBook(Book(title = title, author = author))
}
}
// Toggle a book's read status
fun toggleRead(book: Book) {
viewModelScope.launch {
dao.updateBook(book.copy(isRead = !book.isRead))
}
}
// Remove a book from the database
fun deleteBook(book: Book) {
viewModelScope.launch {
dao.deleteBook(book)
}
}
What does this code do?
- Each function launches a coroutine inside viewModelScope. This scope is automatically cancelled when the ViewModel is cleared, preventing leaked background work.
- The UI never touches Room directly. It calls addBook(), toggleRead(), or deleteBook() and the ViewModel handles the threading.
- Save BookViewModel.kt.
- Confirm Android Studio shows no red underlines in the file. If you see unresolved references, check that your imports match the code above exactly.
Seeing unresolved references?
- Make sure you chose AndroidViewModel (from androidx.lifecycle), not plain ViewModel. The class needs the Application parameter.
- If viewModelScope is red, confirm your project has the lifecycle dependencies included by the Compose template.
- help me fix unresolved references in my BookViewModel
Update MainActivity to use the ViewModel
With the ViewModel ready, you can strip all direct DAO calls and CoroutineScope usage out of your Activity. The Activity's only job now is to wire the ViewModel to the composables.
- Open MainActivity.kt.
- Find and delete these three import lines:
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
- Add this import in their place:
import androidx.lifecycle.viewmodel.compose.viewModel
- Now replace the entire MainActivity class (from class MainActivity down to its closing }) with this simplified version:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BookTrackerTheme {
// Delegate everything to BookTrackerApp
BookTrackerApp()
}
}
}
}
What changed?
The Activity no longer builds the database, creates the DAO, or spawns coroutines. It just calls BookTrackerApp(), which is a composable that owns the UI. All data logic now lives in the ViewModel.
- Directly below the MainActivity class, add the new BookTrackerApp composable by pasting this code:
@Composable
fun BookTrackerApp(viewModel: BookViewModel = viewModel()) {
// Collect the ViewModel's StateFlow as Compose state
val books by viewModel.books.collectAsState()
var showDialog by remember { mutableStateOf(false) }
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = { showDialog = true }) {
Icon(Icons.Default.Add, contentDescription = "Add Book")
}
}
) { innerPadding ->
BookListScreen(
books = books,
onToggleRead = { viewModel.toggleRead(it) },
onDelete = { viewModel.deleteBook(it) },
modifier = Modifier.padding(innerPadding)
)
if (showDialog) {
AddBookDialog(
onDismiss = { showDialog = false },
onConfirm = { title, author ->
viewModel.addBook(title, author)
showDialog = false
}
)
}
}
}
What does this code do?
- viewModel() is a Compose function that retrieves (or creates) the BookViewModel scoped to this Activity. On rotation, it returns the same instance.
- collectAsState() subscribes to the ViewModel's StateFlow and recomposes the UI whenever the book list changes.
- The lambdas (onToggleRead, onDelete, onConfirm) call ViewModel functions instead of spawning raw coroutines.
- The BookListScreen, BookItem, and AddBookDialog composables below stay exactly as they are. No changes needed.
- Save MainActivity.kt.
Build errors after refactoring?
- If viewModel() is unresolved, confirm you added the import androidx.lifecycle.viewmodel.compose.viewModel line.
- If the old CoroutineScope references still appear, double-check you deleted the three old import lines and the entire previous MainActivity class body.
- help me fix build errors after refactoring MainActivity to use ViewModel
- Click the green Run button (or press Shift+F10) to build and launch the app on the Emulator.
You should see your existing book list appear instantly. The app works exactly as before, but the architecture underneath is now clean.
Test rotation survival
Time to confirm the ViewModel actually fixes the rotation problem you saw earlier.
- Add a new book using the + button (pick any title and author).
- Tap the checkbox on one book to mark it as read.
Before you rotate, predict: will your book list survive this time, or will it flash empty like before?
- Rotate the Emulator by pressing Ctrl+Left (or Ctrl+Right) on your keyboard.
Your book list appears immediately with no flash. Every book, including read-status checkmarks, survives the rotation. The ViewModel retained all data while Android destroyed and rebuilt the Activity.
- Rotate back to portrait by pressing Ctrl+Right (or Ctrl+Left) again to confirm data persists across multiple rotations.
Why this approach over dependency injection?
Production apps often use Hilt or Dagger to inject the database into ViewModels. For a first project, AndroidViewModel gives you the Application context directly without any framework setup. You get the same lifecycle benefits with far less ceremony.
App crashes on rotation?
- Make sure you removed ALL CoroutineScope(Dispatchers.IO) calls from MainActivity.kt. Leftover references to the old dao variable will fail to compile.
- Confirm that BookTrackerApp is a top-level composable function, not nested inside the MainActivity class.
- help me debug a crash when rotating the emulator in my Book Tracker app
✔️ Awesome, I've got everything!
Great. Double check you have saved both BookViewModel.kt and MainActivity.kt.
ⓧ I'd like to double check the full code
Here is the complete BookViewModel.kt:
package com.example.booktracker
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class BookViewModel(application: Application) : AndroidViewModel(application) {
private val dao = BookDatabase.getDatabase(application).bookDao()
val books: StateFlow<List<Book>> = dao.getAllBooks()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun addBook(title: String, author: String) {
viewModelScope.launch {
dao.insertBook(Book(title = title, author = author))
}
}
fun toggleRead(book: Book) {
viewModelScope.launch {
dao.updateBook(book.copy(isRead = !book.isRead))
}
}
fun deleteBook(book: Book) {
viewModelScope.launch {
dao.deleteBook(book)
}
}
}
And here is the updated MainActivity.kt. Scroll through each section to confirm yours matches. The BookListScreen, BookItem, and AddBookDialog composables are unchanged from the previous step.
Imports and MainActivity class:
package com.example.booktracker
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
Remaining imports and the class:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.booktracker.ui.theme.BookTrackerTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BookTrackerTheme {
BookTrackerApp()
}
}
}
}
BookTrackerApp composable:
@Composable
fun BookTrackerApp(viewModel: BookViewModel = viewModel()) {
val books by viewModel.books.collectAsState()
var showDialog by remember { mutableStateOf(false) }
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = { showDialog = true }) {
Icon(Icons.Default.Add, contentDescription = "Add Book")
}
}
) { innerPadding ->
BookListScreen(
books = books,
onToggleRead = { viewModel.toggleRead(it) },
onDelete = { viewModel.deleteBook(it) },
modifier = Modifier.padding(innerPadding)
)
if (showDialog) {
AddBookDialog(
onDismiss = { showDialog = false },
onConfirm = { title, author ->
viewModel.addBook(title, author)
showDialog = false
}
)
}
}
}
BookListScreen and BookItem composables:
@Composable
fun BookListScreen(
books: List<Book>,
onToggleRead: (Book) -> Unit,
onDelete: (Book) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(books, key = { it.id }) { book ->
BookItem(
book = book,
onToggleRead = { onToggleRead(book) },
onDelete = { onDelete(book) }
)
}
}
}
@Composable
fun BookItem(
book: Book,
onToggleRead: () -> Unit,
onDelete: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth()
) {
BookItem body and AddBookDialog:
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = book.isRead,
onCheckedChange = { onToggleRead() }
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
) {
Text(
text = book.title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = book.author,
style = MaterialTheme.typography.bodyMedium
)
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "Delete")
}
}
}
}
AddBookDialog composable:
@Composable
fun AddBookDialog(
onDismiss: () -> Unit,
onConfirm: (String, String) -> Unit
) {
var title by remember { mutableStateOf("") }
var author by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Add a Book") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
TextField(
value = title,
onValueChange = { title = it },
label = { Text("Title") },
modifier = Modifier.fillMaxWidth()
)
TextField(
value = author,
onValueChange = { author = it },
label = { Text("Author") },
modifier = Modifier.fillMaxWidth()
)
}
},
confirmButton = {
TextButton(
onClick = { if (title.isNotBlank() && author.isNotBlank()) onConfirm(title, author) }
) {
Text("Save")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
Your app now follows the MVVM pattern: the View (composables) displays data, the ViewModel manages state and logic, and Room acts as the Model layer. This is the same architecture used in production Android apps. Next up, you will take on the Secret Mission and add real-time search filtering to your book list.
Secret mission
Add a Search Bar
Your book list works great with a handful of books. But what happens when you have 50 or 100 titles? In this secret mission, you will add a real-time search bar that filters your books by title or author as you type.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The BookTracker project folder on your computer.
- An Android virtual device (Emulator image) stored locally on your machine.
- A Room database file stored on the virtual device.
Keep everything running
No action needed. Choose this if you are still actively building or want to return to the project soon.
- Your project folder, Emulator image, and database all remain on disk ready for next time.
- The Emulator image takes up approximately 2-5 GB of disk space.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files and Emulator image stay on disk so you can resume later.
- Close the Emulator window if it is still running.
- Close Android Studio. The project and Emulator image remain on disk and will be available when you reopen Android Studio.
Delete - I don't want to use this again
Remove all project resources and start fresh. This reclaims disk space used by the project and the Emulator image.
- Delete the virtual device: in Android Studio, click Tools in the menu bar, then select Device Manager. Find your virtual device in the list, click the Menu icon (three dots), and select Delete. This removes the Emulator image and the Room database stored on it.
- Delete the project folder. Open File Explorer and navigate to your AndroidStudioProjects folder inside your user directory. Right-click the BookTracker folder and select Delete.
Nice Work!
Nice Work!
You just built a fully functional Book Tracker app from scratch using Kotlin and Jetpack Compose. Your app permanently stores books on-device, lets you add and delete entries, tracks your reading progress, and survives every restart without losing a single title.
You've learned how to:
- Build a data-driven Android app with a Room database that persists books locally using Entity, DAO, and Database classes.
- Create a reactive UI with LazyColumn and full CRUD operations (add, display, toggle read status, delete) that updates instantly when the underlying data changes.
- Apply the MVVM architecture pattern with ViewModel and StateFlow so your app survives configuration changes and keeps UI code separate from data logic.
- Secret Mission: Added real-time search filtering using SQL LIKE queries and flatMapLatest to reactively switch between full and filtered book lists.
Ready to quiz yourself?