Todo App Series: Room Database Implementation

Ken Ruiz Inoue
Deuk
Published in
10 min readJan 3, 2024

Introduction

I’m excited to continue our journey in building the Todo App. If you have followed my previous guide, you are already familiar with the nuances of its UI design. Now, it’s time to breathe life into the app! In this tutorial, I will integrate the Room Database to create a fully functional Todo App.

Missed the UI tutorial? Catch up here:

Let’s dive in!

Environment

  • Android Studio Hedgehog | 2023.1.1
  • Compose version: androidx.compose:compose-bom:2023.08.00
  • Mac OS Sonoma 14.1
  • Pixel 5 Emulator

On Today’s Menu

  • Transform the Todo App UI from a static design into a dynamic, data-driven, working application.

Step 1: Integrating Room Database

Acquiring Base Code

Start by downloading the base code from here in case you don’t have the original project already. This will be our foundation for integrating the Room Database.

Configuring Room Database Dependencies

Before integrating Room, we need to include its libraries in our project. Update the build.gradle.kts (:app) file as follows:

plugins {
...
// 1. Kotlin Annotation Processing Tool Plugin
id("org.jetbrains.kotlin.kapt")
}

android {
...
}

dependencies {

// 2. Room Runtime
implementation("androidx.room:room-runtime:2.6.1")
// 3. Room KTX
implementation("androidx.room:room-ktx:2.6.1")
// 4. Room Compiler
kapt("androidx.room:room-compiler:2.6.1")
...
}
  1. Kotlin Annotation Processing Tool Plugin: This plugin is vital for Room’s functionality. It enables Kotlin project to process annotations.
  2. Room Runtime: Required for the core functionality of the Room database. Incorporates the runtime components necessary for Room to operate, including the classes and methods that manage database creation and provide the main access point to the persisted data.
  3. Room KTX: Adds Kotlin extensions (KTX) support to Room, making it more idiomatic and pleasant to use with Kotlin. This enables coroutines and flow, which enhance Room's capabilities, allowing for more concise, readable, and maintainable asynchronous database operations.
  4. Room Compiler: Key component for processing Room’s annotations, such as @Entity, @Dao, etc. This processing generates necessary code at compile-time for Room to function properly, ensuring that the database interactions are type-safe and efficient.

After adding these dependencies, synchronize your project to ensure the libraries are properly imported and ready for use.

You can sync the project by clicking the Sync Now button that appears on Android Studio when thebuild.gradle.kts (:app) file is modified.

Implementing Room Database

With the necessary dependencies set up, let’s lay the groundwork for our Room database.

Update data/TodoItem.kt to define the structure of the Todo items. Decorate the class with @Entity to mark it as a database entity, and set the primary key with auto-increment functionality.

// Your package...

import androidx.room.Entity
import androidx.room.PrimaryKey

// 1. Entity Annotation
@Entity
data class TodoItem(
// 2. Primary Key with Auto-Generate
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
var isDone: Boolean = false
)
  1. Entity Annotation: The @Entity annotation on the TodoItem class informs Room to recognize this class as a database table. Its properties are interpreted as columns, with the class name defaulting as the table name unless explicitly changed. This annotation is essential for Room to map the class to database rows and structure the database accordingly.
  2. Primary Key with Auto-Generate: Annotating the id field with @PrimaryKey(autoGenerate = true) designates it as the unique identifier for each table row. The auto-generate attribute instructs Room to automatically assign incrementing IDs, ensuring uniqueness for each TodoItem. This functionality is key for efficiently managing entries in the database and preventing potential bugs.

Next, in data/TodoDao.kt, define the data access methods. The DAO interface facilitates data operations and provides a clean API for the app.

// Your package...

import androidx.room.*
import kotlinx.coroutines.flow.Flow

// 1. Dao Annotation
@Dao
interface TodoDao {
// 2. Items Flow
@Query("SELECT * FROM TodoItem")
fun getAllTodos(): Flow<List<TodoItem>>

// 3. Insert Operation for Adding or Updating Items
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(todo: TodoItem)

// 4. Delete an Item by TodoItem.id
@Delete
suspend fun delete(todo: TodoItem)
}
  1. Dao Annotation: The @Dao annotation on the TodoDao interface marks it as a Data Access Object in Room. DAOs are responsible for defining the methods that access the database, encapsulating SQL queries and simplifying database interactions. This annotation allows Room to generate the necessary code to perform database operations associated with TodoItem.
  2. Items Flow: This method, annotated with @Query, provides a way to retrieve all TodoItem entries from the database. The returned Flow<List<TodoItem>> is a continuous stream of data that can update the UI whenever the database content changes. Using Flow here leverages Kotlin coroutines for asynchronous database access, improving app performance.
  3. Insert Operation for Adding or Updating Items: This method uses the @Insert annotation to define how new TodoItem entries are added to the database. The onConflict strategy set to REPLACE ensures that if an existing item with the same ID is already in the database, it will be replaced with the new item. However, it might not be ideal for a production environment because of the risk of unintentional data loss. For instance, if an update is intended to modify only a few attributes of an existing item, but the new item is not fully populated, the REPLACE strategy could inadvertently erase the unpopulated fields, leading to partial data loss in the entity. This highlights the need for cautious handling in scenarios requiring precise updates.
  4. Delete an Item by TodoItem.id: The @Delete annotation on this method tells Room to generate code that deletes a given TodoItem from the database. By passing a TodoItem object, its id is used to identify and delete the corresponding entry in the database. This method simplifies the deletion process and ensures data integrity.

Finally, create thedata/AppDatabase.kt file, and implement the RoomDatabase(). This class acts as the main access point for the underlying SQLite database.

// Your package...

import androidx.room.Database
import androidx.room.RoomDatabase

// 1. Database Annotation
@Database(entities = [TodoItem::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
// 2. Abstract Functions
abstract fun todoDao(): TodoDao
}
  1. Database Annotation: The @Database annotation is crucial for defining the Room database. This annotation specifies the entities that belong to the database and the version number of the database. In this case, entities = [TodoItem::class] tells Room that TodoItem is an entity in this database. The version = 1 part indicates the version of the database, which is important for handling database schema migrations.
  2. Abstract Functions: The abstract fun todoDao(): TodoDao inside the AppDatabase class is an abstract function that provides a way to access the DAOs associated with the database. Since AppDatabase extends RoomDatabase, this function enables other parts of your app to access the TodoDao for performing database operations. By defining this as an abstract method, Room will automatically generate the necessary code to provide a concrete implementation of TodoDao.

Step 2: MainViewModel for UI and DB Interaction

This step focuses on bridging the UI with our database operations.

Establishing a Data Repository

Begin by creating data/TodoRepository.kt. This class serves as a centralized hub for data interactions, allowing for flexible data source management.

// Your package...

import kotlinx.coroutines.flow.Flow

// 1. Repository Initialization
class TodoRepository(private val todoDao: TodoDao) {
// 2. Data Retrieval
val allTodos: Flow<List<TodoItem>> = todoDao.getAllTodos()

// 3. Data Modification Methods
suspend fun insert(todo: TodoItem) {
todoDao.insert(todo)
}

suspend fun delete(todo: TodoItem) {
todoDao.delete(todo)
}
}
  1. Repository Initialization: The TodoRepository class is initialized with a TodoDao parameter. This design pattern encapsulates the data layer of your app, allowing TodoRepository to interact with the database through TodoDao.
  2. Data Retrieval: The val allTodos: Flow<List<TodoItem>> property in the repository utilizes the getAllTodos() function from TodoDao to retrieve all todo items. The use of Flow here is significant, as it provides a continuous stream of data that can be observed. This means any changes in the database will be automatically updated in the UI, facilitating real-time data synchronization.
  3. Data Modification Methods: The repository includes functions suspend fun insert(todo: TodoItem) and suspend fun delete(todo: TodoItem) for modifying the data. These functions call insert and delete on the TodoDao, respectively. Being suspend functions, they are designed to be called from a coroutine, ensuring that these database operations are performed without blocking the main thread, thus maintaining a smooth user experience.

Implementing the ViewModel

Next, create data/MainViewModel.kt. This ViewModel provides the necessary methods to the UI layer, facilitating data operations through the repository.

// Your package...

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import YOUR_PACKAGE_NAME.data.TodoItem
import YOUR_PACKAGE_NAME.data.TodoRepository
import kotlinx.coroutines.launch

class MainViewModel(
// 1. ViewModel Initialization
private val repository: TodoRepository,
private val ioDispatcher: CoroutineDispatcher
) : ViewModel() {

// 2. Todo Data Flow
val todos = repository.allTodos

// 3. Todo Operations
fun addTodo(todo: String) =
viewModelScope.launch(ioDispatcher) { repository.insert(TodoItem(title = todo)) }

fun toggleTodo(todoItem: TodoItem) =
viewModelScope.launch(ioDispatcher) { repository.insert(todoItem.copy(isDone = !todoItem.isDone)) }

fun removeTodo(todoItem: TodoItem) =
viewModelScope.launch(ioDispatcher) { repository.delete(todoItem) }
}
  1. ViewModel Initialization: In MainViewModel, the TodoRepository and a CoroutineDispatcher are passed as parameters. The repository provides access to the data layer, allowing the ViewModel to perform database operations through it. The CoroutineDispatcher, typically Dispatchers.IO for database operations, is used to ensure these operations are executed on a background thread, keeping the UI thread free from blocking operations. This setup effectively separates the concerns of data management and UI logic.
  2. Todo Data Flow: In the MainViewModel, the data flow is managed through the todos variable, which is linked to the allTodos Flow from the TodoRepository. This setup establishes a reactive data stream, where updates in the database are automatically reflected in the UI. The use of Kotlin's Flow ensures asynchronous data handling and lifecycle-aware updates, making the UI responsive and up-to-date with the latest data without manual refreshes.
  3. Todo Operations: The functions addTodo, toggleTodo, and removeTodo are defined to perform database operations. They utilize viewModelScope.launch(ioDispatcher) to execute these operations in the repository asynchronously on the specified dispatcher. This approach ensures that the UI remains responsive while handling data operations in the background.

In this structure, TodoRepository abstracts the data source from MainViewModel, making it easier to modify the data source without affecting the ViewModel logic. MainViewModel then exposes data and actions to the UI, maintaining a clean separation of concerns.

Step 3: Configuring MainActivity

This step involves updating MainActivity.kt to set up the database and ViewModel, and then integrating everything to make the app operational.

// Your package...

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.room.Room
import YOUR_PACKAGE_NAME.composables.TodoInputBar
import YOUR_PACKAGE_NAME.composables.TodoItemsContainer
import YOUR_PACKAGE_NAME.data.AppDatabase
import YOUR_PACKAGE_NAME.data.TodoRepository
import YOUR_PACKAGE_NAME.ui.constants.OverlappingHeight
import kotlinx.coroutines.Dispatchers

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. Manual DB Creation
val db = Room
.databaseBuilder(applicationContext, AppDatabase::class.java, "todo-db")
.build()
// 2. Manual MainViewModel Creation
val mainViewModel = MainViewModel(TodoRepository(db.todoDao()), ioDispatcher = Dispatchers.IO)
setContent {
Box(
modifier = Modifier.fillMaxSize()
) {
TodoItemsContainer(
todoItemsFlow = mainViewModel.todos,
// 3. Method Reference Expression
onItemClick = mainViewModel::toggleTodo,
onItemDelete = mainViewModel::removeTodo,
overlappingElementsHeight = OverlappingHeight
)
TodoInputBar(
modifier = Modifier.align(Alignment.BottomStart),
onAddButtonClick = mainViewModel::addTodo
)
}
}
}
}
  1. Manual DB Creation: This code initializes the Room database directly within MainActivity using Room.databaseBuilder. This approach is straightforward and gets things up and running quickly. However, in the next iteration of this tutorial series, we will explore more advanced techniques for database creation. We need to abstract the database initialization into a singleton pattern or utilizing dependency injection frameworks like Hilt. This enhancement will not only streamline our code but also address potential issues related to scalability and testing.
  2. Manual MainViewModel Creation: Following a similar approach as mentioned above with our database setup, in this iteration of our Todo App, the MainViewModel is manually created within MainActivity, where we directly pass a TodoRepository and Dispatchers.IO. This straightforward method suits our current basic application structure. However, just as we plan to refine our database initialization in future tutorials, we will also enhance our ViewModel creation process.
  3. Method Reference Expression: The use of mainViewModel::method references for UI event handling streamlines the code and ensures a clean separation between the UI and business logic, with the ViewModel handling data operations and maintaining state consistency, essential for reactive frameworks like Compose. This approach simplifies event handling in the Compose UI while adhering to best practices in software design.

By running the app, you should now see a functional Todo application. The tasks will be persisted, ensuring data remains intact even after the app is closed.

Functional Todo App!

Next Steps: Embracing UI Tests

This tutorial will delve into the essentials of creating robust UI tests, ensuring our app's functionality meets high standards.

Wrapping Up

You can find the finished code here. Please show your support with a star!

Throughout this tutorial, you have seen how our initial groundwork in designing Compose functions paid off. By future-proofing them with flows and lambdas parameters, we abstracted the business logic effectively, ensuring no need for modification or updates. Our primary focus was on updating MainActivity to seamlessly integrate the database with the UI, showcasing the power of thoughtful, forward-thinking design.

If any part of this guide was unclear or if you find any aspect confusing, I warmly invite you to engage in the comments section. Your questions and discussions enrich our learning community.

Did you find value in this tutorial? A follow and a clap would be greatly appreciated! Your support fuels my passion for sharing knowledge and experiences in the world of Android development.

Looking forward to our next coding adventure. Stay tuned and happy coding!

Discover More

Are you eager for more knowledge? Dive into my Android Compose Tutorials Library. These tutorials cover a wide spectrum of UI design techniques in Jetpack Compose. Each guide is meticulously crafted to enhance your skills and creativity in Android development!

Deuk Services: Your Gateway to Leading Android Innovation

Are you looking to boost your business with top-tier Android solutions?Partner with Deuk services and take your projects to unparalleled heights.

--

--