DEV Community

David González Blanchard
David González Blanchard

Posted on

How to Build a CRUD Android App with Jetpack Compose and Room (Step by Step)

🚀 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
)
Enter fullscreen mode Exit fullscreen mode

🔎 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)
}
Enter fullscreen mode Exit fullscreen mode

👉 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
}
Enter fullscreen mode Exit fullscreen mode

🔁 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
)
Enter fullscreen mode Exit fullscreen mode

Mapper:

fun TaskEntity.toDomain(): Task { ... }
fun Task.toEntity(): TaskEntity { ... }
Enter fullscreen mode Exit fullscreen mode

👉 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)
}
Enter fullscreen mode Exit fullscreen mode

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())
    }
}
Enter fullscreen mode Exit fullscreen mode

🧠 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()
                )
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

👉 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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

👉 UI automatically updates when data changes
👉 No manual refresh needed

📦 Source Code
https://github.com/daviddagb2/TaskMaster/tree/TaskMasterTutorial

Top comments (0)