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() {
abstract fun contactsDao() : ContactDao
companion object {
@Volatile
var INSTANCE : ContactDatabase?= null
fun getDatabaseInstance(context : Context) : ContactDatabase{
val tempInstance = INSTANCE
if (tempInstance!= null ){
return tempInstance
}
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)
{
fun getAllContacts() : LiveData<List<Contacts>>{
return dao.getAllContacts()
}
fun insertContact(contact : Contacts) {dao.insertContact(contact)}
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>() {
class ViewHolder(val binding : ContactsLayoutBinding) : RecyclerView.ViewHolder(binding.root){}
private val dao = ContactDatabase.getDatabaseInstance(context).contactsDao()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ContactsLayoutBinding.inflate(LayoutInflater.from(parent.context),parent, false )
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.contactName.text = list[position].name
holder.binding.contactNumber.text = list[position].number
holder.binding.deleteButton.setOnClickListener{
dao.delete(list[position])
notifyItemRemoved(position)
}
holder.itemView.setOnClickListener{
val intent = Intent(Intent.ACTION_CALL, Uri.parse( "" + list[position].number))
context.startActivity(intent)
}
}
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 lateinit var binding : ActivityCreateContactBinding
val viewModel : ContactViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super .onCreate(savedInstanceState)
binding = ActivityCreateContactBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.save.setOnClickListener{
createContact(it)
}
}
private fun createContact(it: View?) {
val name = binding.etName.text.toString()
val number = binding.etNumber.text.toString()
val data = Contacts( null ,name = name , number = number)
viewModel.addContacts(data)
Toast.makeText( this @CreateContact , "Saved" , Toast.LENGTH_SHORT).show()
startActivity(Intent( this @CreateContact ,MainActivity:: class .java))
}
}
|
Step 11: Write Code for MainActivity
Kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
val viewModel : ContactViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super .onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.floatingActionButton.setOnClickListener{
val intent = Intent( this , CreateContact:: class .java)
startActivity(intent)
}
viewModel.getAllContacts().observe( this , Observer { list->
binding.recyclerView.layoutManager = LinearLayoutManager(applicationContext)
binding.recyclerView.adapter = ContactsAdapter( this ,list)
})
}
}
|
Output:
Last Updated :
06 Feb, 2023
Like Article
Save Article
Share your thoughts in the comments
Please Login to comment...