This post is about how to use Room Database with MVVM Architecture with the help of a simple application.
What is MVVM?
- MVVM architecture is a Model-View-ViewModel architecture.
- It allows separating the user interface logic from the business (or the back-end) logic.
- Its target is to keep UI code simple and free of app logic in order to make it easier to manage and test.
MVVM has mainly the following layers:
Model:
The model represents the data and the business logic of the Application. It consists of the business logic – local and remote data source, model classes, repository.
View:
The view consists of the UI Code(Activity, Fragment), XML. The view role in this pattern is to observe (or subscribe to) a ViewModel observable to get data in order to update UI elements accordingly.
ViewModel:
The ViewModel is a class whose role is to provide data to the UI and survive configuration changes. A ViewModel acts as a communication center between the Repository and the UI.
One of the important implementation strategies of this layer is to decouple it from the View, i.e, ViewModel should not be aware of the view who is interacting with.
Room Database
The Room persistence library provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. In particular, Room provides the following benefits:
- Compile-time verification of SQL queries.
- Convenience annotations that minimize repetitive and error-prone boilerplate code.
- Streamlined database migration paths.
Creating new project
1 . Create a new project by going to File ⇒ New Android Project, select Empty Activity, provide app name, select language to kotlin and then finally click on finish.
2 . Open app-level build.gradle file and add the below changes:
- Add the below libraries under the dependencies section:
dependencies {
//room library
implementation 'androidx.room:room-runtime:2.4.1'
kapt 'androidx.room:room-compiler:2.4.1'
// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:2.4.1"
//ViewModel and livedata
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
}
- Inside the android block, add buildFeatures block with property viewBinding to true.
buildFeatures {
viewBinding true
}
- Inside plugins block, add the below plugin:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
- Finally, click on Sync Now.
Creating Room components (Entity, Dao and Database)
Entity:
Entity is an annotated class that is used to describe a database table when we are working with Room.
Database:
Database serves as the app’s main access point to the underlying SQLite database. It uses DAO to issue queries to the SQLite database.
DAO:
DAO is a Data Access Object which is used for mapping SQL queries to functions.
Creating an Entity
The following code defines an Event entity. Each instance of Event represents a row in an eventTable in the app’s database. Each row represents one entity (or record), and each column represents a value in that entity.
Event.kt
@Entity(tableName = "eventTable")
data class Event(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "event_id")
val id: Int?,
@ColumnInfo(name = "event_title")
val title: String?,
@ColumnInfo(name = "event_description")
val description: String?
)
Creating a DAO(Data access object) class
The following code defines a DAO called EventsDao. EventsDao provides the methods that the rest of the app uses to interact with data in the eventTable.
EventsDao.kt
@Dao
interface EventsDao {
// adds a new entry to our database.
// if some data is same/conflict, it'll be replace with new data
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertEvent(event :Event)
// deletes an event
@Delete
suspend fun deleteEvent(event: Event)
// updates an event.
@Update
suspend fun updateEvent(event: Event)
// read all the events from eventTable
// and arrange events in ascending order
// of their ids
@Query("Select * from eventTable order by event_id ASC")
fun getAllEvents(): LiveData<List<Event>>
// why not use suspend ? because Room does not support LiveData with suspended functions.
// LiveData already works on a background thread and should be used directly without using coroutines
// delete all events
@Query("DELETE FROM eventTable")
suspend fun clearEvent()
//you can use this too, to delete an event by id.
@Query("DELETE FROM eventTable WHERE event_id = :id")
suspend fun deleteEventById(id: Int)
}
Creating Database class
The following code defines an EventDatabase class to hold the database. EventDatabase defines the database configuration and serves as the app’s main access point to the persisted data.
The database class must satisfy the following conditions:
- must be annotated with a @Database annotation that includes an entities array.
- must be an abstract class that extends RoomDatabase.
- for each DAO class that is associated with the database, the database class must define an abstract method that has zero arguments and returns an instance of the DAO class.
EventDatabase.kt
@Database(entities = [Event::class], version = 1, exportSchema = false)
abstract class EventDatabase : RoomDatabase() {
abstract fun getEventsDao(): EventsDao
companion object {
// Volatile annotation means any change to this field
// are immediately visible to other threads.
@Volatile
private var INSTANCE: EventDatabase? = null
private const val DB_NAME = "event_database.db"
fun getDatabase(context: Context): EventDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
// here synchronised used for blocking the other thread
// from accessing another while in a specific code execution.
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
EventDatabase::class.java,
DB_NAME
).build()
INSTANCE = instance
// return instance
instance
}
}
}
}
Setup Data Repository
A Repository is a class that abstracts access to multiple data sources.
The below Repository class contains some methods which invoke the methods of the Dao class.
EventRepositoryt.kt
import androidx.lifecycle.LiveData
import com.c1ctech.mvvmwithroomdb.data.db.EventsDao
import com.c1ctech.mvvmwithroomdb.data.db.entity.Event
class EventRepository(private val eventDao: EventsDao) {
// get all the events
fun getAllEvents(): LiveData<List<Event>> = eventDao.getAllEvents()
// adds an event to our database.
suspend fun insertEvent(event: Event) {
eventDao.insertEvent(event)
}
// deletes an event from database.
suspend fun deleteEvent(event: Event) {
eventDao.deleteEvent(event)
}
// updates an event from database.
suspend fun updateEvent(event: Event) {
eventDao.updateEvent(event)
}
//delete an event by id.
suspend fun deleteEventById(id: Int) = eventDao.deleteEventById(id)
// delete all events
suspend fun clearEvent() = eventDao.clearEvent()
}
Setup the ViewModel
The EventViewModel class extends the AndroidViewModel.
ViewModel is basically used for providing the data to our UI. It acts as a communication layer between Repository and the UI.
EventViewModel.kt
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.c1ctech.mvvmwithroomdb.data.db.EventDatabase
import com.c1ctech.mvvmwithroomdb.data.db.entity.Event
import com.c1ctech.mvvmwithroomdb.data.repository.EventRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class EventViewModel(
application: Application
) : AndroidViewModel(application) {
val allEvents: LiveData<List<Event>>
val repository: EventRepository
// initialize dao, repository and all events
init {
val dao = EventDatabase.getDatabase(application).getEventsDao()
repository = EventRepository(dao)
allEvents = repository.getAllEvents()
}
fun insertEvent(event: Event) =
viewModelScope.launch(Dispatchers.IO) { repository.insertEvent(event) }
fun updateEvent(event: Event) =
viewModelScope.launch(Dispatchers.IO) { repository.updateEvent(event) }
fun deleteEvent(event: Event) =
viewModelScope.launch(Dispatchers.IO) { repository.deleteEvent(event) }
fun deleteEventById(id: Int) =
viewModelScope.launch(Dispatchers.IO) { repository.deleteEventById(id) }
fun clearEvent() =
viewModelScope.launch(Dispatchers.IO) { repository.clearEvent() }
}
Creating Activity Layout file
The below layout file contains a RecyclerView and a FloatingActionButton on click of which we will add an event to the database.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:background="@color/white"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/eventsRV"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="5dp"
tools:listitem="@layout/event_rv_item" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:src="@android:drawable/ic_input_add"
app:tint="@color/white" />
</RelativeLayout>
Creating Adapter Layout file
The below layout file represents the UI of each item of recyclerView.
event_rv_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
app:cardBackgroundColor="@color/teal_700"
app:cardCornerRadius="3dp"
app:cardElevation="3dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:textColor="@android:color/white"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:textColor="@color/white"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
app:layout_constraintVertical_bias="0.0"
tools:text="Description" />
<ImageView
android:id="@+id/imgDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:src="@drawable/ic_delete"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
Creating Adapter class
The EventAdapter is an adapter class for the recycler view to set all the items into recycler view.
EventAdapter.kt
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.c1ctech.mvvmwithroomdb.data.db.entity.Event
import com.c1ctech.mvvmwithroomdb.databinding.EventRvItemBinding
class EventAdapter(
val eventDeleteIconClickInterface: EventDeleteIconClickInterface,
val eventClickInterface: EventClickInterface
) : RecyclerView.Adapter<EventViewHolder>() {
private val allEvents = ArrayList<Event>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = EventRvItemBinding.inflate(inflater, parent, false)
return EventViewHolder(binding)
}
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
// sets data to item of recycler view.
holder.binding.tvTitle.text = allEvents.get(position).title
holder.binding.tvDescription.text = allEvents.get(position).description
// adding click listener to our delete image view icon.
holder.binding.imgDelete.setOnClickListener {
// call eventDeleteIconClickInterface.onEventDeleteIconClick() and pass position to it.
eventDeleteIconClickInterface.onEventDeleteIconClick(allEvents.get(position))
}
// adding click listener to our recycler view item.
holder.itemView.setOnClickListener {
// call eventClickInterface.onEventClick() and pass position to it.
eventClickInterface.onEventClick(allEvents.get(position))
}
}
override fun getItemCount(): Int {
// return list size.
return allEvents.size
}
// update the list of events.
fun updateList(newList: List<Event>) {
// clear the allEvents array list
allEvents.clear()
// adds a new list to our allEvents list.
allEvents.addAll(newList)
// call notifyDataSetChanged() to notify our adapter.
notifyDataSetChanged()
}
}
interface EventDeleteIconClickInterface {
// creating a method for click
// action on delete image view.
fun onEventDeleteIconClick(event: Event)
}
interface EventClickInterface {
// creating a method for click action
// on recycler view item for updating it.
fun onEventClick(event: Event)
}
class EventViewHolder(val binding: EventRvItemBinding) : RecyclerView.ViewHolder(binding.root) {}
Creating AddEventActivity File
Inside AddEventActivity, we will add and edit an event.
AddEventActivity.kt
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import com.c1ctech.mvvmwithroomdb.data.db.entity.Event
import com.c1ctech.mvvmwithroomdb.databinding.ActivityAddEventBinding
class AddEventActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddEventBinding
lateinit var viewModal: EventViewModel
var eventID = -1;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddEventBinding.inflate(layoutInflater)
setContentView(binding.root)
// initializes the viewmodel.
viewModal = ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(application)
).get(EventViewModel::class.java)
// getting data passed via an intent.
val eventType = intent.getStringExtra("eventType")
if (eventType.equals("Edit")) {
// setting data to edit text.
val eventTitle = intent.getStringExtra("eventTitle")
val eventDescription = intent.getStringExtra("eventDescription")
eventID = intent.getIntExtra("eventId", -1)
binding.btnSave.setText("Update Event")
binding.titleET.setText(eventTitle)
binding.descriptionET.setText(eventDescription)
} else {
binding.btnSave.setText("Save Event")
}
// adding click listener to our save button.
binding.btnSave.setOnClickListener {
// getting title and desc from edit text.
val eventTitle = binding.titleET.text.toString()
val eventDescription = binding.descriptionET.text.toString()
// checking the type and then saving or updating the data.
if (eventType.equals("Edit")) {
if (eventTitle.isNotEmpty() && eventDescription.isNotEmpty()) {
val updatedEvent = Event(eventID, eventTitle, eventDescription)
viewModal.updateEvent(updatedEvent)
Toast.makeText(this, "Event Updated", Toast.LENGTH_LONG).show()
}
} else {
if (eventTitle.isNotEmpty() && eventDescription.isNotEmpty()) {
// if the string is not empty we are calling
// add event method to add data to our room database.
//why id null? because id is auto generate
viewModal.insertEvent(Event(null, eventTitle, eventDescription))
Toast.makeText(this, "Event Added", Toast.LENGTH_LONG).show()
}
}
// opening the new activity
startActivity(Intent(applicationContext, MainActivity::class.java))
this.finish()
}
}
}
Creating MainActivity File
Inside MainActivity, we will get an instance of ViewModel and use ViewModel properties which will execute the SQL queries and update the UI accordingly.
MainActivity.kt
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.c1ctech.mvvmwithroomdb.data.db.entity.Event
import com.c1ctech.mvvmwithroomdb.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity(), EventClickInterface, EventDeleteIconClickInterface {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: EventViewModel
private lateinit var eventAdapter: EventAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel = ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(application)
).get(EventViewModel::class.java)
eventAdapter = EventAdapter(this, this)
initView()
observeEvents()
}
private fun initView() {
binding.fabAdd.setOnClickListener {
val intent = Intent(this, AddEventActivity::class.java)
startActivity(intent)
}
binding.eventsRV.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = eventAdapter
}
}
private fun observeEvents() {
viewModel.allEvents.observe(this, Observer { list ->
list?.let {
// updates the list.
eventAdapter.updateList(it)
}
})
}
private fun clearEvent() {
val dialog = AlertDialog.Builder(this, R.style.ThemeOverlay_AppCompat_Dialog)
dialog.setTitle("Clear Event")
.setMessage("Are you sure, you want to delete all event?")
.setPositiveButton(android.R.string.ok) { _, _ ->
viewModel.clearEvent().also {
Toast.makeText(this, "Event Deleted", Toast.LENGTH_LONG).show()
}
}.setNegativeButton(android.R.string.cancel, null).create().show()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.clearEventItem -> clearEvent()
}
return super.onOptionsItemSelected(item)
}
override fun onEventDeleteIconClick(event: Event) {
viewModel.deleteEvent(event)
Toast.makeText(this, "Event Deleted", Toast.LENGTH_LONG).show()
}
override fun onEventClick(event: Event) {
// opening a new intent and passing a data to it.
val intent = Intent(this@MainActivity, AddEventActivity::class.java)
intent.putExtra("eventType", "Edit")
intent.putExtra("eventTitle", event.title)
intent.putExtra("eventDescription", event.description)
intent.putExtra("eventId", event.id)
startActivity(intent)
this.finish()
}
}
When you run the app it will look like this: