
Local data storage is essential for building modern Android apps that work offline and store data persistently. Whether you’re creating a note-taking app, a budgeting app, or a simple to-do list, you’ll want a reliable and clean way to handle local databases. That’s where Room comes into play.
In this article, we’ll walk through how to implement a Room Database in Kotlin using a practical To-Do List App example and walkthrough all the CRUD (Create,Read,Update,Delete) operations. You’ll learn how to define your database schema, write data access objects (DAOs), and use Room with Kotlin coroutines for a modern Android architecture.

What is Room Database in Android?
Room is an abstraction layer built on top of SQLite Database. It simplifies database access and provides compile-time verification of SQL queries. It integrates seamlessly with Kotlin, Coroutines, LiveData, and Flow. Room database has three components :
- Entity: Represents a table in the database.
- DAO (Data Access Object): Provides methods to interact with the database.
- Database: A singleton instance that provides access to the SQlite database.
Entity
Entities is Room database represents tables. Each instance of the class corresponds to a row in the table.
@Entity(tableName = "todo_table")
data class TodoItem(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val description: String,
val isDone: Boolean = false
)
DAO (Data Access Object)
DAO is interface with DAO Annotation that is used to interact with database using methods. Inside DAO we define methods (INSERT, UPDATE, DELETE, FETCH) to perform database operations.
@Dao
interface TodoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTodo(todo: TodoItem)
@Update
suspend fun updateTodo(todo: TodoItem)
@Delete
suspend fun deleteTodo(todo: TodoItem)
@Query("SELECT * FROM todo_table ORDER BY id DESC")
suspend fun getAllTodos(): List
}
Database
The Database class is the main entry point for interacting with the Room database. Here, we tie everything together.
@Database(entities = [TodoItem::class], version = 1, exportSchema = false)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao(): TodoDao
companion object {
@Volatile
private var INSTANCE: TodoDatabase? = null
fun getDatabase(context: Context): TodoDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TodoDatabase::class.java,
"todo_database"
).build()
INSTANCE = instance
instance
}
}
}
}
Step by Step Implementation
Here, we have created a small project of ToDoApp Using Room Database in Android as shown below :
Project Structure

STEP 1: Add Required Dependencies
//add in build.gradle(:app)
plugins {
id 'kotlin-kapt' // ✅ Required for kapt to work
}
//add in build.gradle(:app)
dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
implementation "androidx.room:room-runtime:2.6.1"
implementation "androidx.room:room-ktx:2.6.1"
kapt "androidx.room:room-compiler:2.6.1"
implementation "com.google.android.material:material:1.11.0"
}
STEP 2: Create Entity
File : ToDoItem.kt
package com.vairagicodes.todoapp.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "todo_table")
data class TodoItem(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val isDone: Boolean = false
)
STEP 3: Create DAO
Create a DAO (Data Access Model) for interacting with the Database.
File: TodoDao.kt
package com.vairagicodes.todoapp.data
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
@Dao
interface TodoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(todo: TodoItem)
@Delete
suspend fun delete(todo: TodoItem)
@Update
suspend fun update(todo: TodoItem)
@Query("SELECT * FROM todo_table ORDER BY id DESC")
fun getAllTodos(): LiveData>
}
STEP 4: Create Database Class
File: TodoDatabase.kt
package com.vairagicodes.todoapp.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [TodoItem::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao(): TodoDao
companion object {
@Volatile
private var INSTANCE: TodoDatabase? = null
fun getDatabase(context: Context): TodoDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TodoDatabase::class.java,
"todo_db"
).build()
INSTANCE = instance
instance
}
}
}
}
STEP 5: Repository Layer
File: TodoRepository.kt
package com.vairagicodes.todoapp.repository
import com.vairagicodes.todoapp.data.TodoDao
import com.vairagicodes.todoapp.data.TodoItem
class TodoRepository(private val dao: TodoDao) {
val allTodos = dao.getAllTodos()
suspend fun insert(todo: TodoItem) = dao.insert(todo)
suspend fun delete(todo: TodoItem) = dao.delete(todo)
suspend fun update(todo: TodoItem) = dao.update(todo)
}
STEP 6: ViewModel
File: TodoViewModel.kt
package com.vairagicodes.todoapp.ui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.vairagicodes.todoapp.data.TodoDatabase
import com.vairagicodes.todoapp.data.TodoItem
import com.vairagicodes.todoapp.repository.TodoRepository
import kotlinx.coroutines.launch
class TodoViewModel(application: Application) : AndroidViewModel(application) {
private val repository: TodoRepository
val allTodos: LiveData>
init {
val dao = TodoDatabase.getDatabase(application).todoDao()
repository = TodoRepository(dao)
allTodos = repository.allTodos
}
fun insert(todo: TodoItem) = viewModelScope.launch {
repository.insert(todo)
}
fun delete(todo: TodoItem) = viewModelScope.launch {
repository.delete(todo)
}
fun update(todo: TodoItem) = viewModelScope.launch {
repository.update(todo)
}
}
STEP 7: Create UI for the app
File: activity_main.xml
File: item_todo.xml
File: dialog_add_todo.xml
STEP 8: Create Adapter class for RecyclerView
File: TodoAdapter.kt
package com.vairagicodes.todoapp.ui
import android.graphics.Paint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.ImageButton
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.vairagicodes.todoapp.R
import com.vairagicodes.todoapp.data.TodoItem
class TodoAdapter(
private val onDeleteClick: (TodoItem) -> Unit,
private val onCheckboxToggle: (TodoItem) -> Unit
) : ListAdapter(DiffCallback()) {
inner class TodoViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title = view.findViewById(R.id.todoTitle)
val deleteBtn = view.findViewById(R.id.deleteBtn)
val checkBox = view.findViewById(R.id.checkBox)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_todo, parent, false)
return TodoViewHolder(view)
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val item = getItem(position)
holder.title.text = item.title
holder.deleteBtn.setOnClickListener { onDeleteClick(item) }
holder.checkBox.isChecked = item.isDone
holder.checkBox.setOnCheckedChangeListener(null) // avoid recycling issue
holder.checkBox.setOnCheckedChangeListener { _, isChecked ->
onCheckboxToggle(item.copy(isDone = isChecked))
}
if (item.isDone) {
holder.title.paintFlags = holder.title.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
holder.title.paintFlags = holder.title.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
class DiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(old: TodoItem, new: TodoItem) = old.id == new.id
override fun areContentsTheSame(old: TodoItem, new: TodoItem) = old == new
}
}
STEP 9: MainActivity Class
File: MainActivity.kt
package com.vairagicodes.todoapp
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.vairagicodes.todoapp.data.TodoItem
import com.vairagicodes.todoapp.ui.TodoAdapter
import com.vairagicodes.todoapp.ui.TodoViewModel
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: TodoViewModel
private lateinit var adapter: TodoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById(R.id.todoRecyclerView)
val fab = findViewById(R.id.fab)
adapter = TodoAdapter(
onDeleteClick =
{ todo -> viewModel.delete(todo) },
onCheckboxToggle = { updatedTodo ->
viewModel.update(updatedTodo)
}
)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
viewModel = ViewModelProvider(this)[TodoViewModel::class.java]
viewModel.allTodos.observe(this) { todos ->
adapter.submitList(todos)
}
fab.setOnClickListener {
showAddTodoDialog()
}
}
private fun showAddTodoDialog() {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_add_todo, null)
val editText = view.findViewById(R.id.editTodoTitle)
AlertDialog.Builder(this)
.setTitle("Add Task")
.setView(view)
.setPositiveButton("Add") { _, _ ->
val title = editText.text.toString()
if (title.isNotBlank()) {
viewModel.insert(TodoItem(title = title))
}
}
.setNegativeButton("Cancel", null)
.show()
}
}
If you find this content Helpful feel free to Enroll in our Android Development Course with Kotlin contact us for more information