How to Build a Simple Contact List Android App using MVVM and Room Database?
A contact list app is a great tool to store and manage our contacts in one place. With the rise of smartphones and mobile devices, contact list apps have become increasingly popular. In this article, we will explore the development of a contact list app using Kotlin, Room Database, and MVVM architecture.
Model — View — ViewModel (MVVM) is the industry-recognized software architecture pattern that overcomes all drawbacks of MVP and MVC design patterns. MVVM suggests separating the data presentation logic(Views or UI) from the core business logic part of the application.
The separate code layers of MVVM are:
- Model: This layer is responsible for the abstraction of the data sources. Model and ViewModel work together to get and save the data.
- View: The purpose of this layer is to inform the ViewModel about the user’s action. This layer observes the ViewModel and does not contain any kind of application logic.
- ViewModel: It exposes those data streams which are relevant to the View. Moreover, it serves as a link between the Model and the View.

A sample video is given below to get an idea about what we are going to do in this article.
Step-By-Step Implementation
Step 1: Create a New Project in Android Studio
To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio. Note that select Kotlin as the programming language.
Step 2: Set Up Project dependencies
Add room, view model, and live data dependencies
// Room Database implementation "androidx.room:room-runtime:2.3.0" kapt "androidx.room:room-compiler:2.3.0" implementation "androidx.room:room-ktx:2.3.0" androidTestImplementation "androidx.room:room-testing:2.3.0" // Lifecycle components // MVVM implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" implementation 'androidx.activity:activity-ktx:1.6.1'
Add kotlin-kapt plugin inside plugin{}
plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' }
Enable View Binding
To enable view binding add this code inside the android {} block in build.gradle(app) file
buildFeatures { viewBinding = true }
Step 3: Create an entity class (Contacts) for Room Database
Kotlin
@Entity (tableName = "Contacts" ) class Contacts ( @PrimaryKey (autoGenerate = true ) var id : Int?= null , var name : String, var number : String ) |
Step 4: Create ContactDao interface to perform queries
Kotlin
@Dao interface ContactDao { @Query ( "Select * from Contacts" ) fun getAllContacts() : LiveData<List<Contacts>> @Insert (onConflict = OnConflictStrategy.REPLACE ) fun insertContact(contact : Contacts) @Delete fun delete(contact: Contacts) } |
Step 5: Create Contact Database
The database is defined as an abstract class “ContactDatabase” which extends Room’s “RoomDatabase” class. The class is annotated with “@Database” which tells Room that this is a database class and it should be used to create a database.
Kotlin
@Database (entities = [Contacts:: class ], version = 1 , exportSchema = false ) abstract class ContactDatabase : RoomDatabase() { // Dao interface for the database abstract fun contactsDao() : ContactDao companion object { @Volatile var INSTANCE : ContactDatabase?= null // Singleton instance of the database fun getDatabaseInstance(context : Context) : ContactDatabase{ val tempInstance = INSTANCE if (tempInstance!= null ){ return tempInstance } // Synchronized block to make sure that // only one instance of the database is created synchronized ( this ){ val roomDatabaseInstance = Room.databaseBuilder(context,ContactDatabase:: class .java, "Contacts" ).allowMainThreadQueries().build() INSTANCE = roomDatabaseInstance return roomDatabaseInstance } } } } |
Step 6: Design the layout
activity_main.xml
XML
<? xml version = "1.0" encoding = "utf-8" ?> < androidx.constraintlayout.widget.ConstraintLayout android:layout_width = "match_parent" android:layout_height = "match_parent" tools:context = ".MainActivity" > < androidx.recyclerview.widget.RecyclerView android:id = "@+id/recyclerView" android:layout_width = "match_parent" android:layout_height = "match_parent" app:layout_constraintBottom_toBottomOf = "parent" app:layout_constraintEnd_toEndOf = "parent" app:layout_constraintStart_toStartOf = "parent" app:layout_constraintTop_toTopOf = "parent" /> < com.google.android.material.floatingactionbutton.FloatingActionButton android:id = "@+id/floatingActionButton" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginEnd = "8dp" android:layout_marginBottom = "8dp" android:clickable = "true" app:layout_constraintBottom_toBottomOf = "parent" app:layout_constraintEnd_toEndOf = "parent" app:srcCompat = "@android:drawable/ic_input_add" /> </ androidx.constraintlayout.widget.ConstraintLayout > |
activity_create_contact
XML
<? xml version = "1.0" encoding = "utf-8" ?> < LinearLayout android:layout_width = "match_parent" android:layout_height = "match_parent" android:orientation = "vertical" tools:context = ".CreateContact" > < EditText android:id = "@+id/etName" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:hint = "Enter Name" > </ EditText > < EditText android:id = "@+id/etNumber" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:inputType = "number" android:hint = "Enter Number" > </ EditText > < Button android:id = "@+id/save" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:text = "Save" android:layout_gravity = "center" > </ Button > </ LinearLayout > |
contacts_layout for recycler view
XML
<? xml version = "1.0" encoding = "utf-8" ?> < androidx.cardview.widget.CardView android:layout_width = "match_parent" android:layout_height = "wrap_content" android:elevation = "2dp" android:layout_margin = "10dp" > < LinearLayout android:layout_width = "match_parent" android:layout_height = "wrap_content" android:orientation = "horizontal" android:weightSum = "3" android:background = "@color/white" > < ImageView android:layout_width = "80dp" android:layout_height = "match_parent" android:layout_weight = "1" android:src = "@drawable/ic_baseline_person_24" > </ ImageView > < LinearLayout android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:weightSum = "2" android:layout_weight = "1" android:orientation = "vertical" > < TextView android:id = "@+id/contactName" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:text = "Sangyan Bhagi" android:textSize = "22dp" android:layout_weight = "1" android:layout_marginStart = "5dp" android:textColor = "@color/black" android:textStyle = "bold" > </ TextView > < TextView android:id = "@+id/contactNumber" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:text = "9131902797" android:textSize = "22dp" android:layout_weight = "1" android:layout_marginTop = "5dp" android:layout_marginStart = "5dp" android:textColor = "@color/black" android:textStyle = "bold" > </ TextView > </ LinearLayout > < ImageView android:id = "@+id/deleteButton" android:layout_width = "30dp" android:layout_height = "50dp" android:layout_weight = "1" android:src = "@drawable/ic_baseline_delete_24" > </ ImageView > </ LinearLayout > </ androidx.cardview.widget.CardView > |
Step 7: Create ContactRepository class
The repository class is named “ContactRepository” and it takes in a parameter of type “ContactDao” in its constructor. The ContactRepository class is an abstraction layer between the Room database and the ViewModel, it will handle the data operations.
Kotlin
class ContactRepository(val dao : ContactDao) { // function to get all contacts from the database fun getAllContacts() : LiveData<List<Contacts>>{ return dao.getAllContacts() } // function to insert a contact in the database fun insertContact(contact : Contacts) {dao.insertContact(contact)} // function to delete a contact from the database fun deleteContact(contact: Contacts) { dao.delete(contact) } } |
Step 8: Create a Contact adapter class
The adapter class is named “ContactsAdapter” and it takes in two parameters in its constructor: “context” and “list”. The “context” parameter is used to access the application’s context and the “list” parameter is a list of contacts that will be displayed in the RecyclerView. In the onBindViewHolder method, the contact’s name and number are set to the TextViews of the layout. The delete button onClickListener is set to delete the contact from the database and notify the adapter of the change. Also, the itemView onClickListener is set to make a phone call to the number of the contact.
Kotlin
class ContactsAdapter(val context : Context, val list : List<Contacts>) : RecyclerView.Adapter<ContactsAdapter.ViewHolder>() { // Inner ViewHolder class class ViewHolder(val binding : ContactsLayoutBinding) : RecyclerView.ViewHolder(binding.root){} // DAO instance to interact with the database private val dao = ContactDatabase.getDatabaseInstance(context).contactsDao() // function to inflate the layout for each contact and create a new ViewHolder instance override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( ContactsLayoutBinding.inflate(LayoutInflater.from(parent.context),parent, false ) ) } // function to bind the data to the view elements of the ViewHolder override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.binding.contactName.text = list[position].name holder.binding.contactNumber.text = list[position].number // delete button onClickListener to delete the // contact from the database and notify the // adapter of the change holder.binding.deleteButton.setOnClickListener{ dao.delete(list[position]) notifyItemRemoved(position) } // itemView onClickListener to make a phone call // to the number of the contact holder.itemView.setOnClickListener{ val intent = Intent(Intent.ACTION_CALL, Uri.parse( "" + list[position].number)) context.startActivity(intent) } } // function returns the number of items in the list override fun getItemCount(): Int { return list.size } } |
Step 9: Create Contact View Model
The ViewModel class is named “ContactViewModel” and it takes in a single parameter “application” in its constructor. The ViewModel class extends the “AndroidViewModel” class which is a subclass of the “ViewModel” class that is aware of the application context.
Kotlin
class ContactViewModel(application: Application) : AndroidViewModel(application) { val repository : ContactRepository init { val dao = ContactDatabase.getDatabaseInstance(application).contactsDao() repository = ContactRepository(dao) } fun addContacts(contact : Contacts){ repository.insertContact(contact) } fun getAllContacts() : LiveData<List<Contacts>> = repository.getAllContacts() } |
Step 10: Write Code for CreateContact Activity
Kotlin
class CreateContact : AppCompatActivity() { // private variable to inflate the layout for the activity private lateinit var binding : ActivityCreateContactBinding // variable to access the ViewModel class val viewModel : ContactViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) // inflate the layout binding = ActivityCreateContactBinding.inflate(layoutInflater) setContentView(binding.root) // set onClickListener for save button binding.save.setOnClickListener{ createContact(it) } } // function to create new contact and call // the addContacts function from the ViewModel class private fun createContact(it: View?) { // read name and number from EditTexts val name = binding.etName.text.toString() val number = binding.etNumber.text.toString() // create new contact object val data = Contacts( null ,name = name , number = number) // call addContacts function from the ViewModel class viewModel.addContacts(data) // display a Toast message to confirm the save Toast.makeText( this @CreateContact , "Saved" , Toast.LENGTH_SHORT).show() // start MainActivity startActivity(Intent( this @CreateContact ,MainActivity:: class .java)) } } |
Step 11: Write Code for MainActivity
Kotlin
class MainActivity : AppCompatActivity() { // private variable to inflate the layout for the activity private lateinit var binding: ActivityMainBinding // variable to access the ViewModel class val viewModel : ContactViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) // inflate the layout binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // set onClickListener for the floating action button binding.floatingActionButton.setOnClickListener{ val intent = Intent( this , CreateContact:: class .java) startActivity(intent) } // Observe the LiveData returned by the getAllContacts method viewModel.getAllContacts().observe( this , Observer { list-> // set the layout manager and the adapter for the recycler view binding.recyclerView.layoutManager = LinearLayoutManager(applicationContext) binding.recyclerView.adapter = ContactsAdapter( this ,list) }) } } |
Please Login to comment...