Building a Todo List App with Kotlin and Room Database

This tutorial will guide you through the process of building a Todo List app using Kotlin and Room Database. Kotlin is a modern programming language that provides many powerful features and is becoming increasingly popular among software developers. Room Database is a persistence library provided by Android Jetpack that makes it easy to work with SQLite databases in Android applications. By the end of this tutorial, you will have a fully functional Todo List app that allows users to create, read, update, and delete tasks.

building todo list app kotlin room database

Introduction

What is Kotlin?

Kotlin is a statically typed programming language developed by JetBrains. It is designed to be fully interoperable with Java, which means you can use Kotlin code in your existing Java projects and vice versa. Kotlin provides many features that enhance productivity and make code more concise and expressive. It is also fully supported by Android Studio, making it a great choice for Android app development.

What is Room Database?

Room Database is a part of Android Jetpack, a suite of libraries, tools, and guidance to help developers write high-quality apps more easily. Room Database is a persistence library that provides an abstraction layer over SQLite, allowing you to perform database operations using Kotlin or Java methods instead of writing SQL queries. It also provides compile-time checks and improved performance compared to traditional SQLite operations.

Setting Up the Project

Creating a new Kotlin project

To start building our Todo List app, we need to create a new Kotlin project in Android Studio. Follow these steps:

  1. Open Android Studio and click on "Start a new Android Studio project".
  2. Select "Empty Activity" as the project template and click "Next".
  3. Enter a project name, package name, and choose the programming language as Kotlin.
  4. Click "Finish" to create the project.

Adding Room Database dependency

To use Room Database in our project, we need to add the necessary dependencies in our project's build.gradle file. Open the build.gradle file and add the following lines of code inside the dependencies block:

implementation "androidx.room:room-runtime:2.4.0"
kapt "androidx.room:room-compiler:2.4.0"

These dependencies will provide us with the necessary classes and annotations to work with Room Database.

Creating the Todo List

Designing the UI

Before we start working with the Room Database, let's design the user interface (UI) for our Todo List app. We will use a RecyclerView to display the list of tasks and allow users to interact with them.

Implementing the RecyclerView

To implement the RecyclerView, we need to follow these steps:

  1. Open the activity_main.xml file in the res/layout folder.
  2. Replace the default TextView with the following code:
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/todoRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />

This code creates a RecyclerView with a LinearLayoutManager, which will display the tasks in a vertical list.

  1. Create a new file called todo_item.xml in the res/layout folder and add the following code:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/titleTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/descriptionTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp" />

</LinearLayout>

This code defines the layout for each item in the RecyclerView. It contains two TextViews to display the task title and description.

  1. In the MainActivity.kt file, add the following code inside the onCreate() method:
val todoRecyclerView = findViewById<RecyclerView>(R.id.todoRecyclerView)
todoRecyclerView.layoutManager = LinearLayoutManager(this)
val adapter = TodoAdapter()
todoRecyclerView.adapter = adapter

This code sets up the RecyclerView, its layout manager, and an instance of the TodoAdapter, which we will create next.

Adding CRUD functionality

To add CRUD (Create, Read, Update, Delete) functionality to our Todo List app, we need to define the data model and implement the necessary database operations.

Working with Room Database

Defining the Entity

In Room Database, an entity represents a table in the database. Each instance of the entity represents a row in the table. To define the entity for our Todo List app, follow these steps:

  1. Create a new Kotlin file called Todo.kt and add the following code:
@Entity(tableName = "todos")
data class Todo(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val title: String,
    val description: String
)

This code defines the Todo entity with three properties: id, title, and description. The id property is annotated with @PrimaryKey and autoGenerate = true, which means Room Database will automatically generate a unique id for each Todo object.

  1. Create a new Kotlin file called TodoDao.kt and add the following code:
@Dao
interface TodoDao {
    @Insert
    suspend fun insert(todo: Todo)

    @Update
    suspend fun update(todo: Todo)

    @Delete
    suspend fun delete(todo: Todo)

    @Query("SELECT * FROM todos ORDER BY title ASC")
    fun getAllTodos(): LiveData<List<Todo>>
}

This code defines the TodoDao interface, which specifies the database operations we want to perform. The @Insert, @Update, and @Delete annotations define the corresponding database operations for inserting, updating, and deleting Todo objects. The @Query annotation is used to define custom database queries. In this case, we are querying all the todos and ordering them by title in ascending order.

  1. Create a new Kotlin file called TodoDatabase.kt and add the following code:
@Database(entities = [Todo::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
    abstract fun todoDao(): TodoDao

    companion object {
        private const val DATABASE_NAME = "todo_database"

        @Volatile
        private var INSTANCE: TodoDatabase? = null

        fun getInstance(context: Context): TodoDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    TodoDatabase::class.java,
                    DATABASE_NAME
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

This code defines the TodoDatabase class, which is a subclass of RoomDatabase. It provides an abstract method, todoDao(), which returns an instance of the TodoDao interface. The companion object contains a singleton pattern implementation to ensure that only one instance of the database is created.

Testing the App

Unit testing the database operations

To ensure the correctness of our database operations, we need to write unit tests. Room Database provides in-memory database support for testing, which allows us to test our database operations without affecting the actual database on the device or emulator.

Implementing unit tests

  1. Create a new Kotlin file called TodoDaoTest.kt and add the following code:
@RunWith(AndroidJUnit4::class)
class TodoDaoTest {
    private lateinit var todoDao: TodoDao
    private lateinit var todoDatabase: TodoDatabase

    @Before
    fun setup() {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        todoDatabase = Room.inMemoryDatabaseBuilder(context, TodoDatabase::class.java)
            .allowMainThreadQueries()
            .build()
        todoDao = todoDatabase.todoDao()
    }

    @After
    fun teardown() {
        todoDatabase.close()
    }

    @Test
    fun insertTodo() = runBlocking {
        val todo = Todo(1, "Title", "Description")
        todoDao.insert(todo)

        val todos = todoDao.getAllTodos().getOrAwaitValue()
        assertEquals(1, todos.size)
        assertEquals(todo, todos[0])
    }

    // Implement other unit tests for update, delete, and getAllTodos operations
}

This code sets up the test environment by creating an in-memory database and obtaining an instance of the TodoDao interface. The insertTodo() method tests the insert operation by creating a new Todo object, inserting it into the database, and verifying that the inserted object is returned correctly.

  1. Run the unit tests by right-clicking on the TodoDaoTest class and selecting "Run 'TodoDaoTest'".

UI testing with Espresso

In addition to unit testing the database operations, it is also important to test the user interface (UI) of our Todo List app. Espresso is a testing framework provided by Android that allows us to write UI tests.

Implementing UI tests

  1. Create a new Kotlin file called MainActivityTest.kt and add the following code:
@RunWith(AndroidJUnit4::class)
@LargeTest
class MainActivityTest {

    @Rule
    @JvmField
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun addTodo() {
        onView(withId(R.id.fab)).perform(click())
        onView(withId(R.id.titleEditText)).perform(typeText("New Todo"))
        onView(withId(R.id.descriptionEditText)).perform(typeText("New Todo Description"))
        onView(withId(R.id.addButton)).perform(click())

        onView(withText("New Todo")).check(matches(isDisplayed()))
        onView(withText("New Todo Description")).check(matches(isDisplayed()))
    }

    // Implement other UI tests for update, delete, and sorting/filtering operations
}

This code uses the onView() and perform() functions provided by Espresso to interact with the UI elements of our app. The addTodo() method tests the add operation by clicking on the FloatingActionButton, entering values in the title and description EditTexts, and clicking on the addButton. It then verifies that the newly added Todo object is displayed correctly in the RecyclerView.

  1. Run the UI tests by right-clicking on the MainActivityTest class and selecting "Run 'MainActivityTest'".

Improving the App

Adding sorting and filtering

To enhance the user experience of our Todo List app, we can add sorting and filtering functionality to allow users to organize their tasks more effectively.

Implementing sorting

  1. Add a new method called getAllTodosSortedByTitle() in the TodoDao interface:
@Query("SELECT * FROM todos ORDER BY title ASC")
fun getAllTodosSortedByTitle(): LiveData<List<Todo>>

This method queries all the todos from the database and orders them by title in ascending order.

  1. Modify the adapter in the MainActivity.kt file to use the getAllTodosSortedByTitle() method:
val todos = todoDao.getAllTodosSortedByTitle()
todos.observe(this) { adapter.submitList(it) }

This code observes the LiveData returned by getAllTodosSortedByTitle() and updates the adapter with the new list of todos whenever it changes.

Implementing filtering

  1. Add a new method called getTodosWithTitleContaining() in the TodoDao interface:
@Query("SELECT * FROM todos WHERE title LIKE '%' || :title || '%' ORDER BY title ASC")
fun getTodosWithTitleContaining(title: String): LiveData<List<Todo>>

This method queries the todos from the database that have a title containing the specified string and orders them by title in ascending order.

  1. Modify the adapter in the MainActivity.kt file to use the getTodosWithTitleContaining() method:
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
    override fun onQueryTextSubmit(query: String): Boolean {
        return false
    }

    override fun onQueryTextChange(newText: String): Boolean {
        val todos = todoDao.getTodosWithTitleContaining(newText)
        todos.observe(this@MainActivity) { adapter.submitList(it) }
        return true
    }
})

This code sets an OnQueryTextListener on the SearchView to listen for changes in the query text. Whenever the query text changes, it calls the getTodosWithTitleContaining() method with the new text and updates the adapter with the new list of filtered todos.

Implementing notifications

To provide timely reminders to users about their tasks, we can add notifications to our Todo List app. Notifications can be scheduled to appear at a specific time or when certain conditions are met.

Implementing scheduled notifications

  1. Add the following code in the TodoDao interface to define a new method for getting a Todo by its id:
@Query("SELECT * FROM todos WHERE id = :id")
suspend fun getTodoById(id: Long): Todo?

This method queries the todo from the database with the specified id.

  1. Modify the insert() method in the TodoDao interface to return the id of the inserted Todo object:
@Insert
suspend fun insert(todo: Todo): Long

This change allows us to get the id of the inserted Todo object.

  1. Create a new Kotlin file called NotificationHelper.kt and add the following code:
class NotificationHelper(private val context: Context) {

    private val alarmManager: AlarmManager =
        context.getSystemService(Context.ALARM_SERVICE) as AlarmManager

    fun scheduleNotification(todoId: Long, title: String, description: String, timeInMillis: Long) {
        val intent = Intent(context, AlarmReceiver::class.java).apply {
            putExtra(EXTRA_TODO_ID, todoId)
            putExtra(EXTRA_TITLE, title)
            putExtra(EXTRA_DESCRIPTION, description)
        }
        val pendingIntent = PendingIntent.getBroadcast(
            context,
            todoId.toInt(),
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT
        )
        alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent)
    }

    fun cancelNotification(todoId: Long) {
        val intent = Intent(context, AlarmReceiver::class.java)
        val pendingIntent = PendingIntent.getBroadcast(
            context,
            todoId.toInt(),
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT
        )
        alarmManager.cancel(pendingIntent)
    }

    companion object {
        const val EXTRA_TODO_ID = "todoId"
        const val EXTRA_TITLE = "title"
        const val EXTRA_DESCRIPTION = "description"
    }
}

This code defines the NotificationHelper class, which provides methods for scheduling and canceling notifications. It uses the AlarmManager class to schedule notifications at a specific time.

  1. Create a new Kotlin file called AlarmReceiver.kt and add the following code:
class AlarmReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val todoId = intent.getLongExtra(NotificationHelper.EXTRA_TODO_ID, -1)
        val title = intent.getStringExtra(NotificationHelper.EXTRA_TITLE)
        val description = intent.getStringExtra(NotificationHelper.EXTRA_DESCRIPTION)

        // Show the notification
    }
}

This code defines the AlarmReceiver class, which extends the BroadcastReceiver class. It receives the broadcast sent by the AlarmManager when the scheduled time for the notification is reached.

Handling data synchronization

To ensure that the user's tasks are always up to date, we can add data synchronization functionality to our Todo List app. Data synchronization allows the app to fetch the latest data from a remote server and update the local database accordingly.

Implementing data synchronization

  1. Create a new Kotlin file called TodoRepository.kt and add the following code:
class TodoRepository(private val todoDao: TodoDao) {

    suspend fun syncTodos(todos: List<Todo>) {
        todoDao.deleteAll()
        todoDao.insertAll(todos)
    }

    suspend fun getTodos(): List<Todo> {
        // Fetch todos from the remote server
        return emptyList()
    }
}

This code defines the TodoRepository class, which is responsible for handling data synchronization. The syncTodos() method deletes all existing todos from the local database and inserts the new todos fetched from the remote server. The getTodos() method fetches the latest todos from the remote server.

  1. Modify the insertTodo() method in the MainActivity.kt file to call the syncTodos() method after inserting a new todo:
GlobalScope.launch {
    todoDao.insert(todo)
    val todos = todoDao.getTodos()
    todoRepository.syncTodos(todos)
}

This change ensures that the new todo is synchronized with the remote server after it is inserted into the local database.

Conclusion

In this tutorial, we have learned how to build a Todo List app using Kotlin and Room Database. We started by setting up the project and designing the user interface (UI) with a RecyclerView. We then implemented the database operations using Room Database, including defining the entity, creating the DAO, and implementing database operations. We also wrote unit tests and UI tests to ensure the correctness of our app. Finally, we explored ways to improve the app by adding sorting and filtering, implementing notifications, and handling data synchronization.

By following this tutorial, you should now have a good understanding of how to build a Todo List app with Kotlin and Room Database. Feel free to explore further and add more features to make the app even more powerful and user-friendly. Happy coding!