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
- 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.
- 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.
- 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.
- 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.
- 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 title, note 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
Comments
Post a Comment