Performing Insert, Read, Update, and Delete Operations in Android
Course: Android App Development
Topic: Performing Insert, Read, Update, and Delete Operations (CRUD)
Course Link: Android App Development Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction to CRUD Operations
CRUD stands for Create, Read, Update, and Delete. These four operations form the foundation of data management in many Android applications. They allow users to add new records, view saved information, modify existing records, and remove information that is no longer required.
For example, a student management app may allow users to add student details, view a list of students, edit a student's contact information, and delete a student record. A task manager may use CRUD operations to create tasks, display them, mark them as updated, and remove completed or unwanted tasks.
What Does CRUD Mean?
| Operation |
Meaning |
Example |
| Create (Insert) |
Add a new record to storage. |
Adding a new student to a database. |
| Read (Retrieve) |
Fetch and display stored information. |
Displaying all registered students. |
| Update |
Modify an existing record. |
Changing a student's phone number. |
| Delete |
Remove a record from storage. |
Deleting a student record. |
2. Where Are CRUD Operations Used in Android?
Android applications can perform CRUD operations using several persistence technologies. The best choice depends on the kind of information being stored and how the application needs to access it.
| Technology |
Best Use |
Example |
| Room Database |
Structured local application data |
Tasks, students, products, notes |
| SQLite |
Relational data stored locally |
Records managed with SQL queries |
| DataStore |
Small preference or typed key-value data |
Theme and app settings |
| Internal Files |
File-based content |
Text notes and exported files |
| REST API |
Data managed by a remote server |
User profiles and online orders |
In this lesson, Room is used for the main example because it provides a convenient abstraction over SQLite, supports structured queries, and integrates with Kotlin and Android architecture components.
3. Understanding the Room Database
Room is an Android persistence library that makes working with a local SQLite database easier. It reduces repetitive database code and checks many SQL queries at compile time.
Main Components of Room
- Entity: Represents a database table and defines the fields stored in each record.
- DAO (Data Access Object): Defines methods for inserting, retrieving, updating, and deleting records.
- Database class: Connects the application to the database and exposes DAO objects.
- Repository: Provides a central place for managing data access and business logic.
- ViewModel: Supplies UI-related state and coordinates operations between the UI and data layer.
4. Setting Up Room in an Android Project
For a Kotlin Android application, add the Room runtime, compiler, and Kotlin extensions if needed. The exact dependency versions should match your project's Android Gradle Plugin, Kotlin, and dependency configuration. The following illustrates the common Gradle setup using the Kotlin KSP plugin.
plugins {
id("com.google.devtools.ksp")
}
dependencies {
val roomVersion = "YOUR_COMPATIBLE_ROOM_VERSION"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
}
Replace the placeholder with a compatible Room version and configure the KSP plugin version in your project's Gradle setup. After updating the dependencies, synchronize the project.
5. Creating an Entity for Database Records
An entity defines the structure of a table. In this example, a task record contains an automatically generated ID, a title, a description, and a completion status.
Kotlin Entity Example
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "tasks")
data class Task(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val title: String,
val description: String,
val isCompleted: Boolean = false
)
Explanation
@Entity(tableName = "tasks") defines the database table name.
@PrimaryKey identifies the unique key for each record.
autoGenerate = true allows Room to generate the numeric ID.
title stores the task name.
description stores additional details.
isCompleted records whether the task has been completed.
Each Task object represents one row in the tasks table. The ID helps identify the correct row when performing update or delete operations.
```html
6. Creating the Data Access Object (DAO)
The DAO defines the functions used to communicate with the database. Room provides annotations such as @Insert, @Query, @Update, and @Delete to perform common database operations.
TaskDao.kt
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 TaskDao {
@Insert
suspend fun insertTask(task: Task): Long
@Query("SELECT * FROM tasks ORDER BY id DESC")
fun getAllTasks(): Flow<List<Task>>
@Query("SELECT * FROM tasks WHERE id = :taskId")
suspend fun getTaskById(taskId: Long): Task?
@Update
suspend fun updateTask(task: Task): Int
@Delete
suspend fun deleteTask(task: Task): Int
@Query("DELETE FROM tasks WHERE id = :taskId")
suspend fun deleteTaskById(taskId: Long): Int
}
Understanding the DAO Methods
@Insert inserts a new task into the table.
@Query executes a SQL query to retrieve or modify data.
@Update updates a row matching the entity's primary key.
@Delete deletes a row matching the entity's primary key.
suspend allows a function to be called from a Kotlin coroutine.
Flow<List<Task>> emits the current task list and subsequent changes to the observed table.
- The integer returned by update or delete queries indicates how many rows were affected.
Room generates the DAO implementation during compilation. Using DAO methods helps keep database operations separate from the user interface.
7. Creating the Room Database Class
The database class connects the entity and DAO to the underlying SQLite database. An application commonly maintains one database instance for a single process rather than creating a new instance every time data is accessed.
AppDatabase.kt
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(
entities = [Task::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"task_database"
).build().also { database ->
INSTANCE = database
}
}
}
}
}
Explanation
@Database declares the Room database configuration.
entities = [Task::class] registers the Task entity.
version = 1 identifies the initial database schema version.
taskDao() provides access to task operations.
Room.databaseBuilder() creates the database configuration.
applicationContext avoids unnecessarily retaining an Activity context.
- The singleton pattern avoids creating multiple expensive database instances in a single process.
For production applications, configure schema export and database migrations as appropriate. Do not rely on destructive migrations when existing user data must be preserved.
8. Create Operation: Inserting New Records
The Create operation adds new information to the database. In a task application, a user might enter a task title and description and then select a Save button.
Using the Insert DAO Method
suspend fun addTask(
taskDao: TaskDao,
title: String,
description: String
): Long {
val task = Task(
title = title.trim(),
description = description.trim()
)
require(task.title.isNotBlank()) {
"Task title cannot be empty"
}
return taskDao.insertTask(task)
}
How Insertion Works
- The user enters the information in the application form.
- The application validates the required fields.
- A new Task object is created.
- The DAO receives the object through
insertTask().
- Room inserts the record into the database.
- The returned ID can be used to identify the newly inserted row.
Handling Duplicate Records
Depending on the application, duplicate records may be allowed or prevented. For example, a task manager may allow two tasks with the same title, while a student registration system may require a unique student number.
For cases where uniqueness is required, consider adding a unique index to the entity and handling the resulting conflict appropriately. Do not assume that matching text automatically prevents duplicates.
Example of a Unique Index
@Entity(
tableName = "students",
indices = [Index(value = ["email"], unique = true)]
)
data class Student(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val email: String
)
This is a separate example for a Student entity. It requires importing androidx.room.Index. The unique index prevents multiple rows from storing the same indexed email value.
9. Read Operation: Retrieving Stored Records
The Read operation retrieves information from the database so it can be displayed, searched, filtered, or used in application logic.
Retrieve All Tasks
@Query("SELECT * FROM tasks ORDER BY id DESC")
fun getAllTasks(): Flow<List<Task>>
This query returns tasks ordered by descending ID. The Flow emits updated lists when Room detects changes to the observed table.
Retrieve One Task by ID
@Query("SELECT * FROM tasks WHERE id = :taskId")
suspend fun getTaskById(taskId: Long): Task?
The :taskId parameter binds the supplied ID to the query. A nullable Task result is useful because the requested record may not exist.
Search Tasks by Title
@Query("""
SELECT * FROM tasks
WHERE title LIKE '%' || :searchText || '%'
ORDER BY title ASC
""")
suspend fun searchTasks(searchText: String): List<Task>
This query finds task titles containing the supplied search text. Use parameter binding rather than concatenating user input directly into raw SQL statements.
Count All Tasks
@Query("SELECT COUNT(*) FROM tasks")
suspend fun getTaskCount(): Int
Aggregate queries such as COUNT() are useful for showing summaries, dashboards, and progress indicators.
10. Update Operation: Modifying Existing Records
The Update operation modifies an existing database record. For example, a user may edit a task's title, change its description, or mark it as completed.
Updating an Entity with @Update
suspend fun editTask(
taskDao: TaskDao,
taskId: Long,
newTitle: String,
newDescription: String
): Boolean {
val existingTask = taskDao.getTaskById(taskId)
?: return false
val updatedTask = existingTask.copy(
title = newTitle.trim(),
description = newDescription.trim()
)
require(updatedTask.title.isNotBlank()) {
"Task title cannot be empty"
}
return taskDao.updateTask(updatedTask) > 0
}
How Update Works
- Identify the record that needs to be modified.
- Retrieve the existing record using its primary key.
- Validate the new input values.
- Create an updated entity while preserving its primary key.
- Call the DAO update method.
- Check the number of affected rows to determine whether a record was updated.
Room's @Update method uses the primary key to locate the row. If no row matches that key, the database remains unchanged.
Updating a Specific Column with SQL
Sometimes only one field needs to be changed. A targeted query can update that column without reconstructing the entire entity.
@Query("""
UPDATE tasks
SET isCompleted = :completed
WHERE id = :taskId
""")
suspend fun setTaskCompleted(
taskId: Long,
completed: Boolean
): Int
This method updates the completion status of a task and returns the number of affected rows. If the supplied ID does not match a record, the return value is normally zero.
11. Delete Operation: Removing Records
The Delete operation removes information from the database. Applications should identify the intended record accurately and provide confirmation when deletion could result in the loss of important user data.
Delete an Entity
suspend fun removeTask(
taskDao: TaskDao,
task: Task
): Boolean {
return taskDao.deleteTask(task) > 0
}
Room uses the primary key in the supplied entity to locate the corresponding row.
Delete by ID
@Query("DELETE FROM tasks WHERE id = :taskId")
suspend fun deleteTaskById(taskId: Long): Int
Delete All Tasks
@Query("DELETE FROM tasks")
suspend fun deleteAllTasks(): Int
Deleting all records is a destructive action. Use it only when the application explicitly needs this functionality, and consider asking the user to confirm before proceeding.
Important Delete Considerations
- Use a primary key or another sufficiently specific condition to target the correct record.
- Check the affected-row count when the application needs to confirm that a row was deleted.
- Understand foreign-key relationships before deleting related records.
- Use transactions when multiple changes must succeed or fail together.
- Do not confuse deleting a database row with deleting the entire database file.
12. Understanding CRUD Operations with SQL
CRUD operations can also be expressed using standard SQL statements. Room's annotations provide a convenient way to execute these operations without manually managing database cursors.
| Operation |
SQL Statement |
Purpose |
| Create |
INSERT INTO tasks (title, description) VALUES (?, ?) |
Adds a new record. |
| Read |
SELECT * FROM tasks |
Retrieves records. |
| Update |
UPDATE tasks SET title = ? WHERE id = ? |
Changes a selected record. |
| Delete |
DELETE FROM tasks WHERE id = ? |
Removes a selected record. |
The question mark placeholders represent bound parameters in SQL examples. When using Room, named parameters such as :taskId are commonly used in annotated queries.
13. Connecting CRUD Operations to a Repository
A repository separates data-access logic from the UI. It can coordinate DAO calls and provide a clean interface for a ViewModel.
TaskRepository.kt
class TaskRepository(
private val taskDao: TaskDao
) {
val allTasks: Flow<List<Task>> =
taskDao.getAllTasks()
suspend fun insert(task: Task): Long {
require(task.title.isNotBlank())
return taskDao.insertTask(task)
}
suspend fun getById(id: Long): Task? {
return taskDao.getTaskById(id)
}
suspend fun update(task: Task): Int {
require(task.title.isNotBlank())
return taskDao.updateTask(task)
}
suspend fun delete(task: Task): Int {
return taskDao.deleteTask(task)
}
}
Benefits of Using a Repository
- Separates persistence code from screen code.
- Makes the application easier to test.
- Provides a single place to apply data validation and business rules.
- Can combine local database data with remote API data if required.
- Helps keep ViewModels focused on UI-related state and events.
14. Displaying CRUD Data in an Android UI
The UI collects data from a ViewModel and presents it to the user. In a modern Kotlin application, a Flow from Room can be exposed as observable UI state.
Example ViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class TaskViewModel(
private val repository: TaskRepository
) : ViewModel() {
val tasks = repository.allTasks.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun addTask(title: String, description: String) {
if (title.isBlank()) return
viewModelScope.launch {
repository.insert(
Task(
title = title.trim(),
description = description.trim()
)
)
}
}
fun updateTask(task: Task) {
viewModelScope.launch {
repository.update(task)
}
}
fun deleteTask(task: Task) {
viewModelScope.launch {
repository.delete(task)
}
}
}
This example illustrates the data flow. A complete application should also expose loading and error states and use a ViewModel factory or dependency-injection framework to provide the repository.
Jetpack Compose Example for Reading Tasks
@Composable
fun TaskListScreen(viewModel: TaskViewModel) {
val tasks by viewModel.tasks.collectAsState()
LazyColumn {
items(
items = tasks,
key = { task -> task.id }
) { task ->
Column {
Text(text = task.title)
Text(text = task.description)
Row {
Checkbox(
checked = task.isCompleted,
onCheckedChange = { completed ->
viewModel.updateTask(
task.copy(isCompleted = completed)
)
}
)
Text("Completed")
TextButton(
onClick = {
viewModel.deleteTask(task)
}
) {
Text("Delete")
}
}
}
}
}
}
This screen demonstrates reading, updating, and deleting tasks. Add the appropriate Compose imports and dependencies. The example assumes the ViewModel is supplied through a correctly configured ViewModel provider or dependency-injection setup.
15. Handling Errors During CRUD Operations
Database operations can fail because of invalid input, constraint violations, schema problems, or other unexpected conditions. A reliable application should report errors meaningfully and avoid presenting a failed operation as successful.
| Problem |
Possible Cause |
Recommended Solution |
| Record not found |
Incorrect or outdated ID |
Handle nullable query results and zero affected rows. |
| Duplicate unique value |
Existing record uses the same unique field |
Validate input and handle constraint conflicts. |
| Update has no effect |
Primary key does not match an existing row |
Verify the record ID and check the result. |
| Unexpected data deletion |
Delete query lacks a specific condition |
Review the query and add appropriate filters. |
| UI freezes |
Blocking work on the main thread |
Use supported asynchronous DAO methods and coroutines. |
| Database migration error |
Schema changed without a valid migration |
Implement and test schema migrations. |
| Invalid form values |
Missing or malformed user input |
Validate data before saving. |
Example: Handling an Insert Error
viewModelScope.launch {
try {
repository.insert(
Task(
title = enteredTitle.trim(),
description = enteredDescription.trim()
)
)
// Publish a success state to the UI.
} catch (e: Exception) {
// Publish an appropriate error state to the UI.
// Log details for diagnosis without exposing sensitive data.
}
}
In production code, prefer catching expected exceptions where possible instead of catching every exception indiscriminately. Keep cancellation behavior intact in coroutine-based code and avoid swallowing cancellation exceptions.
16. Using Transactions for Multiple Database Operations
A transaction groups related database operations so that they are treated as a unit. Transactions are useful when several changes must all succeed together or be rolled back together.
For example, transferring an item between two lists may require removing a record from one table and inserting it into another. If only one of these operations succeeds, the database could be left in an inconsistent state.
Room Transaction Example
import androidx.room.Transaction
@Dao
interface TaskDao {
@Insert
suspend fun insertTask(task: Task): Long
@Query("DELETE FROM tasks WHERE id = :taskId")
suspend fun deleteTaskById(taskId: Long): Int
@Transaction
suspend fun replaceTask(oldTaskId: Long, newTask: Task) {
deleteTaskById(oldTaskId)
insertTask(newTask)
}
}
Room runs a DAO method annotated with @Transaction within a transaction. If the method throws an exception, the transaction is rolled back. In this example, the old record is deleted before the new record is inserted, so use this pattern only if replacing the record is truly the intended business operation.
17. CRUD Operations with a REST API
CRUD operations are not limited to a local database. An Android app can also send HTTP requests to a server to create, retrieve, update, and delete remote resources.
| CRUD Operation |
Common HTTP Method |
Example Endpoint |
| Create |
POST |
POST /api/tasks |
| Read all |
GET |
GET /api/tasks |
| Read one |
GET |
GET /api/tasks/10 |
| Update or replace |
PUT |
PUT /api/tasks/10 |
| Partial update |
PATCH |
PATCH /api/tasks/10 |
| Delete |
DELETE |
DELETE /api/tasks/10 |
These are common conventions; the actual methods and endpoints depend on the server's API design. Remote operations require network error handling, authentication where applicable, and appropriate response-status handling.
Local Database vs. Remote API
- Local Room database: Stores data on the device and supports offline access.
- Remote API: Exchanges data with a backend server and may support multiple users or devices.
- Combined approach: A local database can cache server data and support offline-first workflows, with synchronization managed separately.
18. Security and Data Integrity Best Practices
- Validate user input before creating or updating records.
- Use parameterized Room queries rather than assembling SQL from untrusted input.
- Use primary keys to identify records reliably.
- Apply database constraints where appropriate to enforce uniqueness and valid relationships.
- Request confirmation before destructive actions when user data could be lost.
- Do not store passwords or authentication secrets in plain-text database fields.
- Use appropriate authentication and authorization for remote CRUD endpoints.
- Use transactions for operations that must remain consistent as a group.
- Test schema migrations before releasing database updates.
- Provide clear loading, success, empty, and error states in the UI.
19. Testing CRUD Operations
CRUD functions should be tested to ensure that each operation works as expected and that invalid conditions are handled safely.
Suggested Test Cases
| Test |
Expected Result |
| Insert a valid task |
A new record is stored and can be retrieved. |
| Read all tasks when the table is empty |
An empty list is returned. |
| Read a task using a valid ID |
The matching task is returned. |
| Read a task using a missing ID |
A nullable query returns no record. |
| Update an existing task |
The stored fields reflect the changes. |
| Update a nonexistent ID |
No row is changed. |
| Delete an existing task |
The record is no longer returned by a query. |
| Delete a nonexistent ID |
The affected-row count is zero. |
| Insert duplicate unique data |
The configured conflict behavior is followed. |
| Reopen the database |
Persisted records remain available. |
For reliable automated testing, use an in-memory Room database for isolated DAO tests where appropriate. Instrumentation tests can verify database behavior in an Android environment.
20. Practical Mini Project: Task Management App
Build a task management application that demonstrates all four CRUD operations using Room and a simple Android interface.
Project Features
- Add a task with a title and description.
- Display all saved tasks in a list.
- Open a task and view its details.
- Edit a task's title or description.
- Mark a task as completed or incomplete.
- Delete an individual task.
- Display an empty-state message when there are no tasks.
- Display clear validation and error messages.
Recommended Development Steps
- Create the Android project using Kotlin.
- Configure Room dependencies and the KSP plugin.
- Create the Task entity.
- Define insert, read, update, and delete methods in TaskDao.
- Create the AppDatabase class.
- Implement a repository for database access.
- Create a ViewModel to manage UI state and coroutine operations.
- Build a form for entering and editing tasks.
- Display tasks in a list using RecyclerView or Jetpack Compose.
- Add update and delete actions for each task.
- Test empty inputs, missing IDs, database persistence, and error scenarios.
21. Common Mistakes to Avoid
- Forgetting to declare an entity in the Room database configuration.
- Using an entity without a valid primary key.
- Attempting to update a record without preserving its primary key.
- Assuming that a successful function call means a row was changed without checking the result when needed.
- Running blocking database work on the main thread.
- Deleting all records because a query is missing its WHERE condition.
- Ignoring schema migrations after changing entities.
- Mixing UI logic and database logic in one large class.
- Displaying success messages before the operation has actually succeeded.
- Failing to handle empty data, missing records, or database errors.
22. Interview Questions and Answers
Q1. What does CRUD stand for?
Answer: CRUD stands for Create, Read, Update, and Delete. These operations cover the basic lifecycle of stored records.
Q2. What is Room in Android?
Answer: Room is a persistence library that provides an abstraction over SQLite and simplifies local database access.
Q3. What is an Entity?
Answer: An entity is a class annotated with @Entity that represents a database table.
Q4. What is a DAO?
Answer: A Data Access Object defines methods for interacting with a database, including inserting, querying, updating, and deleting records.
Q5. What is the purpose of @Insert?
Answer: @Insert tells Room to generate code for inserting entity objects into the corresponding table.
Q6. How does @Update identify a row?
Answer: Room uses the primary key of the supplied entity to identify the row that should be updated.
Q7. What is the difference between @Delete and a DELETE query?
Answer: @Delete deletes rows based on entity primary keys. A @Query with a DELETE statement allows the developer to specify conditions in SQL.
Q8. What does Flow provide in Room?
Answer: A Flow can emit query results and later updates as the observed database tables change.
Q9. Why should database operations be asynchronous?
Answer: Database work can take time. Asynchronous execution helps prevent UI blocking and improves responsiveness.
Q10. What is a database transaction?
Answer: A transaction groups related operations so they are committed together or rolled back if the transaction fails.
Q11. What is the purpose of a repository?
Answer: A repository separates data-access logic from UI logic and provides a central interface for data operations.
Q12. When should a developer use a remote API instead of only Room?
Answer: A remote API is appropriate when data must be managed on a server, shared across devices, or accessed by multiple users. Room can still be used locally for caching and offline access.
23. Quick Revision Checklist
- CRUD means Create, Read, Update, and Delete.
- Room provides an abstraction over SQLite.
- Entities define the database tables.
- DAOs define database operations.
@Insert adds new records.
@Query retrieves or modifies data through SQL.
@Update modifies rows matched by primary key.
@Delete removes rows matched by primary key.
- Use nullable results when a requested record may not exist.
- Use Flow for observable query results when appropriate.
- Use coroutines and suitable asynchronous APIs to keep the UI responsive.
- Use transactions for related operations that must remain consistent.
- Validate input and handle errors before showing success feedback.
- Test every CRUD operation, including missing-record and invalid-input cases.
24. Conclusion
Insert, Read, Update, and Delete operations are fundamental to building data-driven Android applications. By using Room entities, DAOs, database classes, repositories, and ViewModels, developers can organize data access into clear and maintainable components. Correct validation, asynchronous execution, transaction management, error handling, and testing help ensure that stored information remains accurate and that the application provides a reliable user experience.
Continue learning with the Android App Development course at JustAcademy, visit the JustAcademy website, or register for a course demo.
Additional learning resource: Official Android documentation on accessing data with Room DAOs.