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
Declare Gradle dependencies and plugins. Refer documentation.
Create Database entities(tables) using @Entity annotation.
Create DAO (Data Access Object) interface with @Dao annotation. Define its methods to perform CRUD operation.
Create the database with @Database annotation which includes entities array list and version. define abstract methods for each of the DAO classes.
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.