MVVM architecture, ViewModel and LiveData
What is MVVM?

Model-View-ViewModel (MVVM) is a software design pattern that is structured to separate program logic and user interface controls.
MVVM is basically composed of three layers :
- Model : This holds the data of the application. It cannot directly talk to the View. Generally, it’s recommended to expose the data to the ViewModel through Observables.
- View : It represents the UI of the application devoid of any Application Logic. It observes the ViewModel.
- ViewModel : It acts as a link between Model and View. It is responsible for transforming the data from the model. Prepares observable(s) observable by View.. It also uses hooks or callbacks to update the View. It will request data from the model.
We can easily see what kind of interactions exist between these three components on the following simple graph.

LiveData
LiveData
is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.
The advantages of using LiveData
- Ensures your UI matches your data state
- No memory leaks
- No crashes due to stopped activities
- No more manual lifecycle handling
- Always up to date data
- Proper configuration changes
- Sharing resources
Now let’s see how I used it in my messaging app.
Using LiveData with Domain Layer or Repository Layer

We have to use a different layer to get the data.
Repository can have instances of :
- Remote i.e. network
- Database
- Cache or Shared Preferences.
Repository
There is an interface where tasks are defined for messaging and a class where we use this interface.

an example repository interface
interface UserRepositoryI {
fun showListOfUser(userList : ArrayList<Users>)
}
I used firebase for storage.
class UserRepository(UserRepositoryI: UserRepositoryI) {
private var userRepositoryI : UserRepositoryI ?= UserRepositoryI
private var userList = ArrayList<Users>()
fun getUserFirebase(){
val ref = FirebaseDatabase.getInstance().getReference("/users")
ref.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
userList.clear()
snapshot.children.forEach{
val user = it.getValue(Users::class.java)
if (user!=null && user.uid != FirebaseAuth.getInstance().uid){
userList.add(user)
userRepositoryI?.showListOfUser(userList)
}
}
}
override fun onCancelled(error: DatabaseError) {
Log.d("ViewModel",error.message)
}
})
}
}
Model
Lets create a data model class
@Entity
@Parcelize
class Users(
@PrimaryKey(autoGenerate = false)
var uid : String ,
@ColumnInfo(name = "username")
var username:String,
@ColumnInfo(name = "profileImageURL")
var profileImageURL:String,
@ColumnInfo(name = "status")
var status : String,
@ColumnInfo(name = "Email")
var Email : String,
@ColumnInfo(name = "activeState")
var activeState : String ,
@ColumnInfo(name = "token")
var token : String?) : Parcelable{
constructor() : this("","","","","","offline","")
companion object{
private val addition = Addition()
@JvmStatic
@BindingAdapter("imageUrl")
fun loadImage(view : CircleImageView , imageUrl : String?){
imageUrl?.let {
addition.picassoUseIt(imageUrl,view)
}
}
}
}
- Room — It is an ORM provided by Google, which provides an abstraction layer between the SQLite database and our data in the form of objects. It gives us errors in compile-time, which is much better than run-time error which difficult to track and debug.
In order to use the room, it’s very important to define our schema. We do that by creating a data model class and add an @entity annotation. We also must add a @PrimaryKey annotation to our entity’s id.
ViewModel
ViewModel object acts as an intermediate between View and the Model, meaning it provides data for the UI components like fragments or activities. It also includes an observable data holder called LiveData that allows ViewModel to inform or update the View whenever the data get updated. It is very crucial, mainly to keep our app from reloading on orientation changes. Which ultimately provides a great user experience.
Here’s an example,
class UserListViewModel(application: Application) : BaseViewModel(application), UserRepositoryI {
private var userRepository = UserRepository(this)
val users = MutableLiveData<List<Users>>()
val userLoading = MutableLiveData<Boolean>()
val informationMessage = MutableLiveData<Boolean>()
private val specialSharedPreferences = SpecialSharedPreferences(getApplication())
private var updateTimeValue = 0.1 * 60 * 1000 * 1000 * 1000L
fun getAllUsers() : LiveData<List<Users>>{
return users
}
fun getUser(){
val getTime = specialSharedPreferences.getTime()
if (getTime !=null && getTime!=0L && System.nanoTime()-getTime<updateTimeValue){
//get SqLite
getDataSQlite()
}
else{
//get Firebase
userRepository.getUserFirebase()
getAllUsers().value?.let { saveSQLite(it) }
}
}
private fun getDataSQlite(){
launch {
val userList = UsersDatabase(getApplication()).usersDao().getAllUser()
if (userList.isEmpty()){
userRepository.getUserFirebase()
getAllUsers().value?.let { saveSQLite(it) }
}
users.value = userList
informationMessage.value = userList.isEmpty()
userLoading.value = false
}
}
override fun showListOfUser(userList: ArrayList<Users>) {
users.value = userList
informationMessage.value = userList.size==0
userLoading.value = false
}
private fun saveSQLite(userList : List<Users>) {
launch {
val dao = UsersDatabase(getApplication()).usersDao()
dao.deleteAllUser()
dao.insertAllUser(*userList.toTypedArray())
}
specialSharedPreferences.saveTime(System.nanoTime())
}
}
View
We observe the observable live data coming through the viewmodel on the view side.
private fun observeLiveData(){
viewModel.users.observe(viewLifecycleOwner, Observer {
it?.let {
binding.newInformationTV.visibility = View.GONE
binding.recyclerView4.visibility = View.VISIBLE
adapter.UsersListUpdate(it)
}
})
viewModel.informationMessage.observe(viewLifecycleOwner, Observer {
it?.let {
if (it){
binding.newInformationTV.text = "Kullanıcı Listen Boş"
}
else{
binding.newInformationTV.text = ""
}
}
})
viewModel.userLoading.observe(viewLifecycleOwner, Observer {
it?.let {
if (it){
binding.newLoadingBar.visibility = View.VISIBLE
}
else
{
binding.newLoadingBar.visibility = View.GONE
}
}
})
}
Recycler View Adapter
We show the live data we observe on the view side with adapter.
class NewMessagesRVAdapter(private val userList: ArrayList<Users>) : RecyclerView.Adapter<NewMessagesRVAdapter.NewMessageViewHolder>(),Filterable{
var userFilterList = ArrayList<Users>()
lateinit var mContext: Context
init {
userFilterList = userList
}
private val addition = Addition()
class NewMessageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
var itemImage : ImageView = itemView.findViewById(R.id.message_imageView)
var itemTitle : TextView = itemView.findViewById(R.id.message_TV)
var itemStatus: TextView = itemView.findViewById(R.id.status_TV)
var itemProgressBar : ProgressBar = itemView.findViewById(R.id.new_messages_progressBar)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewMessageViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.user_row_new_message,parent,false)
return NewMessageViewHolder(view)
}
override fun getItemCount(): Int {
return userFilterList.size
}
override fun onBindViewHolder(holder: NewMessageViewHolder, position: Int){
holder.itemTitle.text = userFilterList[position].username
holder.itemStatus.text = userFilterList[position].status
addition.picassoUseIt(userFilterList[position].profileImageURL,holder.itemImage,holder.itemProgressBar)
holder.itemProgressBar.visibility = View.GONE
holder.itemView.setOnClickListener {
val action = NewMessagesFragmentDirections.actionNewMessagesFragmentToChatLogFragment(
position,
userFilterList[position].uid,
userFilterList[position].username,
userFilterList[position].profileImageURL,
userFilterList[position].status,
userFilterList[position].activeState,
userFilterList[position].Email,
userFilterList[position].token!!
)
Navigation.findNavController(it).navigate(action)
}
}
@SuppressLint("NotifyDataSetChanged")
fun UsersListUpdate(NewUserList : List<Users>){
userList.clear()
userList.addAll(NewUserList)
notifyDataSetChanged()
}
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val charSearch = constraint.toString()
if (charSearch.isEmpty()) {
userFilterList = userList
} else {
val resultList = ArrayList<Users>()
for (row in userList) {
if (row.username.lowercase(Locale.ROOT).contains(charSearch.lowercase(Locale.ROOT))) {
resultList.add(row)
}
}
userFilterList = resultList
}
val filterResults = FilterResults()
filterResults.values = userFilterList
return filterResults
}
@SuppressLint("NotifyDataSetChanged")
@Suppress("UNCHECKED_CAST")
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
userFilterList = results?.values as ArrayList<Users>
notifyDataSetChanged()
}
}
}
}
Result
One an easily make android apps without following this architecture, but if we want to make apps that are robust, testable, maintainable and easy to read, then we must use this to our advantage.
Thanks for reading and if you like the article, remember to clap.