Todo App Series: Room Database Implementation
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")
...
}
- Kotlin Annotation Processing Tool Plugin: This plugin is vital for Room’s functionality. It enables Kotlin project to process annotations.
- 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.
- 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.
- 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
)
- Entity Annotation: The
@Entity
annotation on theTodoItem
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. - 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 eachTodoItem
. 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)
}
- Dao Annotation: The
@Dao
annotation on theTodoDao
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 withTodoItem
. - Items Flow: This method, annotated with
@Query
, provides a way to retrieve allTodoItem
entries from the database. The returnedFlow<List<TodoItem>>
is a continuous stream of data that can update the UI whenever the database content changes. UsingFlow
here leverages Kotlin coroutines for asynchronous database access, improving app performance. - Insert Operation for Adding or Updating Items: This method uses the
@Insert
annotation to define how newTodoItem
entries are added to the database. TheonConflict
strategy set toREPLACE
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. - Delete an Item by
TodoItem.id
: The@Delete
annotation on this method tells Room to generate code that deletes a givenTodoItem
from the database. By passing aTodoItem
object, itsid
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
}
- 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 thatTodoItem
is an entity in this database. Theversion = 1
part indicates the version of the database, which is important for handling database schema migrations. - Abstract Functions: The
abstract fun todoDao(): TodoDao
inside theAppDatabase
class is an abstract function that provides a way to access the DAOs associated with the database. SinceAppDatabase
extendsRoomDatabase
, this function enables other parts of your app to access theTodoDao
for performing database operations. By defining this as an abstract method, Room will automatically generate the necessary code to provide a concrete implementation ofTodoDao
.
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)
}
}
- Repository Initialization: The
TodoRepository
class is initialized with aTodoDao
parameter. This design pattern encapsulates the data layer of your app, allowingTodoRepository
to interact with the database throughTodoDao
. - Data Retrieval: The
val allTodos: Flow<List<TodoItem>>
property in the repository utilizes thegetAllTodos()
function fromTodoDao
to retrieve all todo items. The use ofFlow
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. - Data Modification Methods: The repository includes functions
suspend fun insert(todo: TodoItem)
andsuspend fun delete(todo: TodoItem)
for modifying the data. These functions callinsert
anddelete
on theTodoDao
, respectively. Beingsuspend
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) }
}
- ViewModel Initialization: In
MainViewModel
, theTodoRepository
and aCoroutineDispatcher
are passed as parameters. Therepository
provides access to the data layer, allowing the ViewModel to perform database operations through it. TheCoroutineDispatcher
, typicallyDispatchers.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. - Todo Data Flow: In the
MainViewModel
, the data flow is managed through thetodos
variable, which is linked to theallTodos
Flow from theTodoRepository
. 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. - Todo Operations: The functions
addTodo
,toggleTodo
, andremoveTodo
are defined to perform database operations. They utilizeviewModelScope.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
)
}
}
}
}
- Manual DB Creation: This code initializes the Room database directly within
MainActivity
usingRoom.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. - 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 withinMainActivity
, where we directly pass aTodoRepository
andDispatchers.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. - 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.
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.