How to use Room Library for Data Storage in Android Apps

What is Room?

ROOM is a persistence library that provides an abstraction layer/wrapper over the SQLite database.

Why Room?

A simpler and cleaner approach to app development because -

a>Reduces the use of error-prone boilerplate code

b>Provides annotations to perform database operations

c>Compile time query verification helps in reducing crashes at run-time.

Room Implementation steps

  1. Declare Gradle dependencies and plugins. Refer documentation.

  2. Create Database entities(tables) using @Entity annotation.

  3. Create DAO (Data Access Object) interface with @Dao annotation. Define its methods to perform CRUD operation.

  4. Create the database with @Database annotation which includes entities array list and version. define abstract methods for each of the DAO classes.

  5. To use this, create an instance of the above database using a database builder from the Room library.

    val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database - name").build()

Example

Let us implement the above-mentioned steps in a simple app for better understanding. This app follows MVVM architectural pattern to store and display student information.

Add gradle dependencies:

Create a data class to store student details. Annotate it with @Entity and give the table name as desired. If the table name is not mentioned, it will be created with the data class name.

@Entity(tableName = "student_details")
data class Student (
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val name: String,
    val age: Int,
    val contact: Int,
    val email:String )

Create a Data Access Object to perform CRUD operations on the table created.

@Dao
interface StudentDao {
    @Query("SELECT * FROM student_details")
    fun getStudent(): Flow<List<Student>>
    @Insert
    suspend fun insertStudent(student:Student)
    @Delete
    suspend fun deleteAll()
    @Query("DELETE FROM student_details where name=:name")
    suspend fun deleteStudent(name:String)
}

Create database instance

//Database Annotation should have entity name ,version number and exportSchema
@Database(entities=arrayOf(Student::class),version=1, exportSchema = false)
abstract class StudentDatabase: RoomDatabase() {

    //Creating abstract function to expose the Dao.
    abstract fun studentDao(): StudentDao

    //Create single instance of the database. If the database instance is available, use that else create it using databaseBuilder.
        companion object{
            @Volatile
            private var INSTANCE: StudentDatabase? = null

            fun getDatabase(context: Context): StudentDatabase{
            return INSTANCE ?: synchronized(this){
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    StudentDatabase::class.java,
                    "student_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

After incorporating the above steps Room is ready for use. Now let us work on other features required for the app.

create a repository class. This is not part of the Architecture components but is recommended best practice. The Repository class helps to maintain a clean API for data access to the rest of the application.

class StudentRepository(private val studentDao: StudentDao) {
    val studentList: Flow<List<Student>> = studentDao.getStudent()
    suspend fun insert(student: Student){
       studentDao.insertStudent(student)
    }
    suspend fun deleteAll(){
        studentDao.deleteAll()
    }
    suspend fun deleteStudent(name: String){
        studentDao.deleteStudent(name)
    }
}

We need only one instance of database and repository in our app. So let us create one application class with a database and repository as its members. This way both members will be retrieved only when they are needed.

class StudentApplication: Application() {
    val database by lazy {StudentDatabase.getDatabase(this)}
    val repository by lazy {StudentRepository(database.studentDao())}
}

Add name field in AndroidManifest file.

<application
    android:name=".StudentApplication"
    android:allowBackup="true"
......
</application>

Create a ViewModel class to hold and process the UI data. ViewModel keeps your data intact during configuration changes. Give repository access to view the model by passing it as a parameter.

class StudentViewModel(private val repository: StudentRepository): ViewModel() {
   val studentList: LiveData<List<Student>> =       
                    repository.studentList.asLiveData()

    fun insertStudent(student: Student) = viewModelScope.launch{
       repository.insert(student)
    }

   fun deleteStudent(name:String) = viewModelScope.launch{
      repository.deleteStudent(name)
   }

   fun deleteAll() = viewModelScope.launch{
      repository.deleteAll()
   }

}

To display the student list in a recycler view create an adapter class called StudentListAdapter.

class StudentListAdapter : ListAdapter<Student, StudentListAdapter.StudentViewHolder>(StudentsComparator()) {

    class StudentViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {

        private val studentItemView: TextView = itemView.findViewById(R.id.student_name)

        fun bind(text: String?) {
            studentItemView.text = text
        }

        companion object {
            fun create(parent: ViewGroup): StudentViewHolder {
                val view: View = LayoutInflater.from(parent.context)
                    .inflate(R.layout.list_item, parent, false)
                return StudentViewHolder(view)
            }
        }

    }


    class StudentsComparator: DiffUtil.ItemCallback<Student>(){
        override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean {
            return oldItem === newItem
        }

        override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean {
           return oldItem.name == newItem.name
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudentViewHolder {
        return StudentViewHolder.create(parent)
    }

    override fun onBindViewHolder(holder: StudentViewHolder, position: Int) {
        val currentItem = getItem(position)
        holder.bind(currentItem.name)
    }
}

Create the required XML layout files. In our app, we have two screens one to display the students and another one to enter new student details. One resource file for the list item of recycler view.

a. 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:layout_margin="16dp"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/student_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />

    <TextView
        android:id="@+id/studentList"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:layout_below="@+id/student_list"/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_add_student"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@drawable/ic_add_students"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"/>


</RelativeLayout>

b. activity_add_student.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:layout_margin="16dp"
    tools:context=".AddStudentActivity">

    <LinearLayout
        android:id="@+id/name_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="Name"
            android:textSize="24dp" />

        <EditText
            android:id="@+id/name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="text"
            android:hint="Student Full Name" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/age_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:layout_below="@+id/name_layout"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="Age"
            android:textSize="24dp" />

        <EditText
            android:id="@+id/age"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Student Age" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/contact_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:layout_below="@+id/age_layout"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="Ph No."
            android:textSize="24dp" />

        <EditText
            android:id="@+id/contact"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Parent's Contact Number" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/email_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:layout_below="@+id/contact_layout"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="Email Id"
            android:textSize="24dp" />

        <EditText
            android:id="@+id/email"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Parent's Email Id" />

    </LinearLayout>

    <Button
        android:id="@+id/save_button"
        android:layout_width="250dp"
        android:layout_height="wrap_content"
        android:text="Save"
        android:layout_below="@+id/email_layout"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="78dp" />

    <Button
        android:id="@+id/delete_button"
        android:layout_width="250dp"
        android:layout_height="wrap_content"
        android:text="Delete"
        android:layout_below="@+id/save_button"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="78dp" />

</RelativeLayout>

c.list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

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

</LinearLayout>

MainActivity: Add logic to display the student list using the recycler view. On click of FAB new screen is opened to enter student details.

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel:StudentViewModel by viewModels{
        StudentViewModelFactory((application as StudentApplication).repository)
    }

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

        val recyclerView = binding.studentList
        val adapter = StudentListAdapter()
        recyclerView.adapter = adapter
        recyclerView.layoutManager=LinearLayoutManager(this)

        // Add an observer on the LiveData 
        viewModel.studentList.observe(this) { students ->
          students.let{ adapter.submitList(it) }
        }

        val fab = binding.fabAddStudent
        fab.setOnClickListener {
            val intent = Intent(this,AddStudentActivity::class.java)
       startActivity(intent)
        }
    }
}

AddStudentActivity: This activity contains logic to insert and delete student data.


class AddStudentActivity : AppCompatActivity() {

    private lateinit var binding: ActivityAddStudentBinding
    private val viewModel: StudentViewModel by viewModels {
        StudentViewModelFactory(StudentRepository(StudentDatabase.getDatabase(this).studentDao()))

    }

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

        val btnSave = binding.saveButton
        btnSave.setOnClickListener {

            //fetch values entered.
            val name = binding.name.text.toString()
            val age = binding.age.text.toString().toInt()
            val phone = binding.contact.text.toString().toInt()
            val email = binding.email.text.toString()

            val student = Student(0, name, age, phone, email)

            viewModel.insertStudent(student)
            Toast.makeText(this, "Student Data Inserted", Toast.LENGTH_SHORT)

            clearData()
        }
        val btnDelete  = binding.deleteButton
        btnDelete.setOnClickListener {
            viewModel.deleteAll()
        }
    }

    private fun clearData() {
        binding.name.text.clear()
        binding.age.text.clear()
        binding.contact.text.clear()
        binding.email.text.clear()
    }
}

The app would look something like below. Table data can be viewed from database inspector.