Open In App

How to Build a Simple Contact List Android App using MVVM and Room Database?

Improve
Improve
Like Article
Like
Save
Share
Report

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.
separate code layers of MVVM

 

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
    xmlns:tools="http://schemas.android.com/tools"
    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
    xmlns:tools="http://schemas.android.com/tools"
    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)
        })
    }
}


Output:



Last Updated : 06 Feb, 2023
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads