DEV Community

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)