Todo List App with Room Database, Kotlin MVVM architecture

 Todo List App with Room Database, Kotlin MVVM architecture

Todo List App with Room Database, Kotlin MVVM architecture

Introduction

Room is a persistence library provided by Google as part of the Android Jetpack suite of libraries. It allows developers to easily create and manage a local SQLite database on an Android device using Kotlin or Java.

RoomDB provides an abstraction layer over SQLite, making it easier for developers to interact with the database without having to write complex SQL queries. It offers compile-time checks for SQL queries, ensuring errors are caught at compile-time instead of runtime.

Why Room Database

  1. Easy to use: Room provides an easy-to-use API for performing database operations on SQLite databases. It handles a lot of the complexity of managing a SQLite database, such as opening and closing connections, managing transactions, and creating and upgrading the schema.
  2. Compile-time checks: Room provides compile-time checks for your SQL queries, which can help catch errors before you run your app. This can save you a lot of time and frustration when developing your app.
  3. Abstraction layer: Room provides an abstraction layer over SQLite, which means that you don’t have to write raw SQL queries to perform common database operations. Instead, you can define simple DAO interfaces and let Room handle the implementation details.
  4. Type safety: Room provides type-safe access to your database. This means that you can use Kotlin data classes to represent your database entities, and Room will generate the necessary SQL code to read and write data to and from the database.
  5. Integration with other Jetpack libraries: Room is part of the Android Jetpack suite of libraries, which means that it integrates seamlessly with other Jetpack libraries such as LiveData and ViewModel. This makes it easy to build robust and scalable Android applications.

Components in the Room Database

Entities: 

Entities are the data models that represent the tables in the database. Each entity corresponds to a table in the database and is annotated with the @Entity annotation. Entities can include fields, primary keys, and indexes.


@Entity(tableName = "todo_table"
data class Todo(
    @PrimaryKey(autoGenerate = true) val id: Int?,
    @ColumnInfo(name = "title") val title: String?,
    @ColumnInfo(name = "note") val note: String?,
    @ColumnInfo(name = "date") val date: String
): java.io.Serializable)

Data Access Objects (DAOs): 

DAOs provide an interface for interacting with the database. They define the SQL queries that will be used to access, insert, update, or delete data in the database. Each DAO is annotated with the @Dao annotation.


@Dao
interface TodoDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(todo: Todo)

    @Delete
    suspend fun delete(todo: Todo)

    @Query("SELECT * from todo_table order by id ASC")
    fun getAllTodos(): LiveData<List<Todo>>

    @Query("UPDATE todo_table set title = :title, note = :note where id = :id")
    suspend fun update(id: Int?, title: String?, note: String?)
}

Database: 

The database is the container for all the data in the Room database. It is defined as an abstract class that extends the RoomDatabase class. The database class is annotated with the @Database annotation and includes a list of all the entities in the database and the version of the database.


@Database(entities = arrayOf(Todo::class), version = 1
abstract class TodoDatabase : RoomDatabase() {
    abstract fun getTodoDao(): 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,
                    DATABASE_NAME
                ).build()

                INSTANCE = instance
                instance
            }
        }
    }

})

Getting Started Implementation

Here is the coding part. let’s get started coding!!

First of all, add the dependencies to app.gradle file

    def room_version = "2.5.1"

    implementation "androidx.room:room-ktx:$room_version"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'

After that, let’s define our Entity Todo. Create a Kotlin data class and add the following code. 

package com.example.todolist.models

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

@Entity(tableName = "todo_table")
data class Todo(
    @PrimaryKey(autoGenerate = true) val id: Int?,
    @ColumnInfo(name = "title") val title: String?,
    @ColumnInfo(name = "note") val note: String?,
    @ColumnInfo(name = "date") val date: String
): java.io.Serializable

Im using titlenote and date as fields in the todo objects. Here you can see I have given todo_table as the table name for the database table. Since every recode should have a primary key I have added autogenerating the primary key as id.

The next step is to implement the (DAO)Data Access Object to indicate operations methods. For that, I’m going to create an interface as TodoDao 

package com.example.todolist.database

import androidx.lifecycle.LiveData
import androidx.room.*
import com.example.todolist.models.Todo

@Dao
interface TodoDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(todo: Todo)

    @Delete
    suspend fun delete(todo: Todo)

    @Query("SELECT * from todo_table order by id ASC")
    fun getAllTodos(): LiveData<List<Todo>>

    @Query("UPDATE todo_table set title = :title, note = :note where id = :id")
    suspend fun update(id: Int?, title: String?, note: String?)
}

I have included all the local database operating methods in the above interface. In the insert function, I have used OnConflictStrategy called REPLACE. That is when some conflict occurs during the insertion of data into the local database we can specify a strategy to use. Im replacing the same record with new data.

The next thing is to define the database connection class. For that create a class called TodoDatabase and add the following code

package com.example.todolist.database

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.todolist.models.Todo
import com.example.todolist.utils.DATABASE_NAME

@Database(entities = arrayOf(Todo::class), version = 1)
abstract class TodoDatabase : RoomDatabase() {
    abstract fun getTodoDao(): 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,
                    DATABASE_NAME
                ).build()

                INSTANCE = instance
                instance
            }
        }
    }

}

In this file, we are creating a database instance. This is a singleton event in that we only create one instance. If the database instance is not created only we create a new instance.

The next thing is to create the repository. Create a class called TodoRepository to define database methods to expose to the viewModel since we following the MVVM architecture.

package com.example.todolist.database

import androidx.lifecycle.LiveData
import com.example.todolist.models.Todo

class TodoRepository(private val todoDao: TodoDao) {

    val allTodos: LiveData<List<Todo>> = todoDao.getAllTodos()

    suspend fun insert(todo: Todo){
        todoDao.insert(todo)
    }

    suspend fun delete(todo: Todo){
        todoDao.delete(todo)
    }

    suspend fun update(todo: Todo){
        todoDao.update(todo.id, todo.title, todo.note)
    }
}

The next task is to create the ViewModel class. It is not essential to create ViewModel when using room db. But we are using the MVVM architecture in this project we have to follow the structure. So create a class as TodoViewmodel and insert the below code

package com.example.todolist.models

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.example.todolist.database.TodoDatabase
import com.example.todolist.database.TodoRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class TodoViewModel(application: Application): AndroidViewModel(application) {
    private val repository: TodoRepository
    val allTodo : LiveData<List<Todo>>

    init {
        val dao = TodoDatabase.getDatabase(application).getTodoDao()
        repository = TodoRepository(dao)
        allTodo = repository.allTodos
    }

    fun insertTodo(todo: Todo) = viewModelScope.launch(Dispatchers.IO){
        repository.insert(todo)
    }

    fun updateTodo(todo: Todo) = viewModelScope.launch(Dispatchers.IO){
        repository.update(todo)
    }

    fun deleteTodo(todo: Todo) = viewModelScope.launch(Dispatchers.IO){
        repository.delete(todo)
    }
}

We have to use the adapter class to display the list item in a recycler view. For that let’s create an adapter class called TodoAdapter

package com.example.todolist.adaptors

import android.content.Context
import android.view.LayoutInflater
import android.view.TextureView
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
import com.example.todolist.R
import com.example.todolist.models.Todo

class TodoAdapter(private val context: Context,val listener: TodoClickListener):
    RecyclerView.Adapter<TodoAdapter.TodoViewHolder>(){

    private val todoList = ArrayList<Todo>()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoAdapter.TodoViewHolder {
        return TodoViewHolder(
            LayoutInflater.from(context).inflate(R.layout.list_item, parent, false)
        )
    }

    override fun onBindViewHolder(holder: TodoAdapter.TodoViewHolder, position: Int) {
       val item = todoList[position]
        holder.title.text = item.title
        holder.title.isSelected = true
        holder.note.text = item.note
        holder.date.text = item.date
        holder.date.isSelected = true
        holder.todo_layout.setOnClickListener {
            listener.onItemClicked(todoList[holder.adapterPosition])
        }
    }

    override fun getItemCount(): Int {
        return todoList.size
    }

    fun updateList(newList: List<Todo>){
        todoList.clear()
        todoList.addAll(newList)
        notifyDataSetChanged()
    }

    inner class TodoViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
        val todo_layout = itemView.findViewById<CardView>(R.id.card_layout)
        val title = itemView.findViewById<TextView>(R.id.tv_title)
        val note = itemView.findViewById<TextView>(R.id.tv_note)
        val date = itemView.findViewById<TextView>(R.id.tv_date)
    }

    interface TodoClickListener {
        fun onItemClicked(todo: Todo)
    }
}

In the above adapter class, we are creating a layout for binding each todo item to the layout. I will add that layout at the bottom. Before that let's create UIs

Then we are going to create 2 activities. One for listing the todos. Other one to Insert, update and delete todos. Let's create AddTodoActivity first

package com.example.todolist

import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Toast
import com.example.todolist.databinding.ActivityAddTodoBinding
import com.example.todolist.models.Todo
import java.text.SimpleDateFormat
import java.util.*

class AddTodoActivity : AppCompatActivity() {

    private lateinit var binding: ActivityAddTodoBinding
    private lateinit var todo: Todo
    private lateinit var oldTodo: Todo
    var isUpdate = false


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityAddTodoBinding.inflate(layoutInflater)
        setContentView(binding.root)

        try {
            oldTodo = intent.getSerializableExtra("current_todo") as Todo
            binding.etTitle.setText(oldTodo.title)
            binding.etNote.setText(oldTodo.note)
            isUpdate = true
        }catch (e: Exception){
            e.printStackTrace()
        }
        if(isUpdate){
            binding.imgDelete.visibility = View.VISIBLE
        }else{
            binding.imgDelete.visibility = View.INVISIBLE
        }

        binding.imgCheck.setOnClickListener {
            val title = binding.etTitle.text.toString()
            val todoDescription = binding.etNote.text.toString()

            if(title.isNotEmpty() && todoDescription.isNotEmpty()){
                val formatter = SimpleDateFormat("EEE, d MMM yyyy HH:mm a")

                if(isUpdate){
                    todo = Todo(oldTodo.id, title, todoDescription, formatter.format(Date()))
                }else{
                    todo = Todo(null, title, todoDescription, formatter.format(Date()))
                }
                var intent = Intent()
                intent.putExtra("todo", todo)
                setResult(Activity.RESULT_OK, intent)
                finish()
            }else{
                Toast.makeText(this@AddTodoActivity, "please enter some data", Toast.LENGTH_LONG).show()
                return@setOnClickListener
            }
        }

        binding.imgDelete.setOnClickListener {
            var intent = Intent()
            intent.putExtra("todo", oldTodo)
            intent.putExtra("delete_todo", true)
            setResult(Activity.RESULT_OK, intent)
            finish()
        }

        binding.imgBackArrow.setOnClickListener {
            onBackPressed()
        }
    }
}

In the above activity, we added some listeners to the buttons. Based on those we pass functions to do the specific action to the database side. 

We are using the MainActivity as the displaying of todo list. below is the code of MainActivity

package com.example.todolist

import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.todolist.adaptors.TodoAdapter
import com.example.todolist.database.TodoDatabase
import com.example.todolist.databinding.ActivityMainBinding
import com.example.todolist.models.Todo
import com.example.todolist.models.TodoViewModel

class MainActivity : AppCompatActivity(), TodoAdapter.TodoClickListener {

    private lateinit var binding: ActivityMainBinding
    private lateinit var database: TodoDatabase
    lateinit var viewModel: TodoViewModel
    lateinit var adapter: TodoAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initUI()

        viewModel = ViewModelProvider(
            this,
            ViewModelProvider.AndroidViewModelFactory.getInstance(application)
        ).get(TodoViewModel::class.java)

        viewModel.allTodo.observe(this) { list ->
            list?.let {
                adapter.updateList(list)
            }
        }

        database = TodoDatabase.getDatabase(this)
    }

    private fun initUI() {
        binding.recyclerView.setHasFixedSize(true)
        binding.recyclerView.layoutManager =
            LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        adapter = TodoAdapter(this, this)
        binding.recyclerView.adapter = adapter

        val getContent =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
                if (result.resultCode == Activity.RESULT_OK) {
                    val todo = result.data?.getSerializableExtra("todo") as? Todo
                    if (todo != null) {
                        viewModel.insertTodo(todo)
                    }
                }
            }

        binding.fabAddTodo.setOnClickListener {
            val intent = Intent(this, AddTodoActivity::class.java)
            getContent.launch(intent)
        }

    }

    private val updateOrDeleteTodo =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                val todo = result.data?.getSerializableExtra("todo") as Todo
                val isDelete = result.data?.getBooleanExtra("delete_todo", false) as Boolean
                if (todo != null && !isDelete) {
                    viewModel.updateTodo(todo)
                }else if(todo != null && isDelete){
                    viewModel.deleteTodo(todo)
                }
            }
        }



    override fun onItemClicked(todo: Todo) {
        val intent = Intent(this@MainActivity, AddTodoActivity::class.java)
        intent.putExtra("current_todo", todo)
        updateOrDeleteTodo.launch(intent)
    }
}

This is the final part of the implementation. We have to create layout files to display the UI. add list_item layout to attached to the recycler view

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
    android:id="@+id/card_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_margin="8dp"
    android:background="@color/white"
    app:cardCornerRadius="8dp"
    android:elevation="8dp">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/tv_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentStart="true"
                android:ellipsize="marquee"
                android:marqueeRepeatLimit="marquee_forever"
                android:padding="8dp"
                android:scrollHorizontally="true"
                android:singleLine="true"
                android:text="Title"
                android:textColor="@color/white"
                android:textSize="18sp"
                android:textStyle="bold"/>
        </RelativeLayout>
        <TextView
            android:id="@+id/tv_note"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:maxLines="10"
            android:padding="8dp"
            android:textStyle="normal"
            android:text="Note"
            android:textColor="@color/white"
            android:textSize="14sp"/>
        <TextView
            android:id="@+id/tv_date"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:ellipsize="marquee"
            android:marqueeRepeatLimit="marquee_forever"
            android:scrollHorizontally="true"
            android:textStyle="normal"
            android:maxLines="1"
            android:padding="8dp"
            android:text="Date"
            android:textColor="@color/white"
            android:textSize="14sp"/>
    </LinearLayout>

</androidx.cardview.widget.CardView>

The next layout is activity_add_todo.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".AddTodoActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:minHeight="?attr/actionBarSize"
        android:theme="?attr/actionBarTheme"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/img_back_arrow"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="8dp"
                app:srcCompat="@drawable/baseline_arrow_back_24" />

            <ImageView
                android:id="@+id/img_check"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentEnd="true"
                android:layout_marginEnd="12dp"
                android:padding="8dp"
                app:srcCompat="@drawable/baseline_check_24" />

            <ImageView
                android:id="@+id/img_delete"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="12dp"
                android:layout_toLeftOf="@+id/img_check"
                android:padding="8dp"
                app:srcCompat="@drawable/baseline_delete_24" />

        </RelativeLayout>
    </androidx.appcompat.widget.Toolbar>

    <EditText
        android:id="@+id/et_title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:background="@null"
        android:ems="10"
        android:hint="Title"
        android:textSize="26sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar" />

    <EditText
        android:id="@+id/et_note"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="26dp"
        android:background="@null"
        android:ems="10"
        android:gravity="top"
        android:hint="Todo Note"
        android:inputType="textMultiLine"
        android:lineSpacingMultiplier="1.8"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="@+id/et_title"
        app:layout_constraintStart_toStartOf="@+id/et_title"
        app:layout_constraintTop_toBottomOf="@+id/et_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

final one is activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/black"
    android:minHeight="?attr/actionBarSize"
    android:theme="?attr/actionBarTheme"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">
    <TextView
        android:textColor="@color/white"
        android:layout_gravity="center"
        android:textStyle="bold"
        android:textSize="24sp"
        android:text="Todo List"
        android:gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</androidx.appcompat.widget.Toolbar>
   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@id/toolbar"
       tools:listitem="@layout/list_item"/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_add_todo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="26dp"
        android:layout_marginBottom="26dp"
        android:clickable="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@drawable/baseline_add_24"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

Run the project and try to play with todo items

No alt text provided for this image


Comments