๐ How to Build a CRUD Android App with Jetpack Compose and Room (Step by Step)
If you're learning Android development, one of the most important skills you need is data persistence.
Many beginner apps look goodโฆ but once you close them, everything is gone.
In this guide, you'll learn how to build a real CRUD app (Create, Read, Update, Delete) using:
- Jetpack Compose (UI)
- Room (local database)
- MVVM + Repository (clean architecture)
By the end, you'll have a solid base you can reuse in real projects.
๐ฅ Full Video Tutorial
You can follow the complete step-by-step implementation here:
๐ https://www.youtube.com/watch?v=UjfbqBYJMM4
๐ง What Weโre Building
A simple Task Manager app that allows you to:
- Create tasks
- View tasks
- Edit tasks
- Delete tasks
- Mark tasks as completed
- Persist data locally using Room
๐๏ธ Architecture Overview
We use a clean and scalable structure:
UI (Compose) โ ViewModel โ Repository โ Room (DAO)
Layers:
- Presentation โ UI + ViewModel
- Domain โ Models + Repository interface
- Data โ Room (Entity, DAO, Database)
This separation keeps your app maintainable and easy to extend.
๐ฆ Step 1 โ Define the Entity (Room Table)
@Entity(tableName = "tasks")
data class TaskEntity(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val description: String,
val isCompleted: Boolean,
val createdAt: Long
)
๐ Step 2 โ Create the DAO
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks ORDER BY createdAt DESC")
fun getAllTasks(): Flow<List<TaskEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: TaskEntity)
@Update
suspend fun updateTask(task: TaskEntity)
@Delete
suspend fun deleteTask(task: TaskEntity)
}
๐ DAO = Data Access Object
๐ Defines how you interact with the database
๐๏ธ Step 3 โ Create the Database
@Database(
entities = [TaskEntity::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
๐ Step 4 โ Domain Model + Mapper
We separate the database model from the domain model:
data class Task(
val id: Int,
val title: String,
val description: String,
val isCompleted: Boolean,
val createdAt: Long
)
Mapper:
fun TaskEntity.toDomain(): Task { ... }
fun Task.toEntity(): TaskEntity { ... }
๐ This avoids coupling your entire app to Room.
๐ Step 5 โ Repository Pattern
Interface:
interface TaskRepository {
fun getAllTasks(): Flow<List<Task>>
suspend fun insertTask(task: Task)
suspend fun updateTask(task: Task)
suspend fun deleteTask(task: Task)
}
Implementation:
class TaskRepositoryImpl(
private val taskDao: TaskDao
) : TaskRepository {
override fun getAllTasks(): Flow<List<Task>> {
return taskDao.getAllTasks().map { list ->
list.map { it.toDomain() }
}
}
override suspend fun insertTask(task: Task) {
taskDao.insertTask(task.toEntity())
}
override suspend fun updateTask(task: Task) {
taskDao.updateTask(task.toEntity())
}
override suspend fun deleteTask(task: Task) {
taskDao.deleteTask(task.toEntity())
}
}
๐ง Step 6 โ ViewModel
class TaskViewModel(
private val repository: TaskRepository
) : ViewModel() {
val tasks: StateFlow<List<Task>> = repository.getAllTasks()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun addTask(title: String, description: String) {
viewModelScope.launch {
repository.insertTask(
Task(
title = title,
description = description,
isCompleted = false,
createdAt = System.currentTimeMillis()
)
)
}
}
}
๐ ViewModel connects UI with data
๐ Uses StateFlow for reactive updates
๐จ Step 7 โ UI with Jetpack Compose
@Composable
fun TaskListScreen(viewModel: TaskViewModel) {
val tasks by viewModel.tasks.collectAsStateWithLifecycle()
LazyColumn {
items(tasks) { task ->
Text(task.title)
}
}
}
๐ UI automatically updates when data changes
๐ No manual refresh needed
๐ฆ Source Code
https://github.com/daviddagb2/TaskMaster/tree/TaskMasterTutorial
Top comments (0)