// EDIT 2024: I have made an example project that is using Jetpack Compose so check it out from my GitHub (whatminjacodes) if you are interested!
This is a super small and simple example project for showing how Model-View-ViewModel (MVVM) architecture can be implemented in Kotlin!
I feel quite often that even the simple example projects have many unnecessary libraries or features so I wanted to do (almost) as simple project as possible! I did add fragments here but it's only because I personally like to use them. If you don't want to use fragments, just add what's in MainFragment to MainActivity instead!
This project only contains MainActivity, MainFragment, MainViewModel and DataModel. MainActivity opens MainFragment which contains text and one button. When that button is clicked, MainViewModel gets the updated text from DataModel and triggers an UI update in MainFragment using LiveData.
Let's get started!
Create a new project
Create a new Android Studio project with Empty Activity and go to the dependencies (build.gradle file). Add the needed dependencies.
def lifecycle_version = "2.2.0"
def fragments_version = "1.2.5"
// viewmodel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// fragments
implementation "androidx.fragment:fragment-ktx:$fragments_version"
Here's an Android documentation which explains more about:
ViewModels
Fragments
I'm adding View Binding here because it makes writing code that interacts with views a lot easier. You can read more about it from here. But it basically just removes the need to use findViewById() and using View Binding is the recommended way to do it since Kotlin extensions are getting deprecated this year.
Project structure
Here's an image of the project structure. All data related files should go into model, all UI related files to view and all ViewModels into viewmodel.
Add MainActivity.kt and activity_main.xml
Let's add MainActivity and it's layout next!
package com.minjee.basicmvvmexample.view
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.add
import androidx.fragment.app.commit
import com.minjee.basicmvvmexample.R
/*
* MainActivity
* - opens our fragment which has the UI
*/
class MainActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
// Adds our fragment
supportFragmentManager.commit {
setReorderingAllowed(true)
add<MainFragment>(R.id.fragment_container_view)
}
}
}
}
MainActivity basically only opens our MainFragment. I like using fragments so if you don't want that, just normally create an activity and add the code we write in MainFragment in MainActivity instead! You might have to do some small changes but the basic idea is same.
<!-- Has the fragment container view for displaying our fragment -->
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
We just have a container for displaying fragments here. If you don't want to use fragments, then just add what we write in fragment_main.xml in activity_main.xml instead. You might have to do some small changes here too but again, the basic idea is same.
Add MainFragment.kt and fragment_main.xml
Next we add a fragment and it's layout.
package com.minjee.basicmvvmexample.view
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import com.minjee.basicmvvmexample.R
import com.minjee.basicmvvmexample.databinding.FragmentMainBinding
import com.minjee.basicmvvmexample.viewmodel.MainViewModel
/*
* MainFragment
* - shows the UI
* - listens to viewModel for updates on UI
*/
class MainFragment: Fragment() {
// View Binding
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
// Create a viewModel
private val viewModel: MainViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMainBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupClickListeners()
fragmentTextUpdateObserver()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
// Setup the button in our fragment to call getUpdatedText method in viewModel
private fun setupClickListeners() {
binding.fragmentButton.setOnClickListener { viewModel.getUpdatedText() }
}
// Observer is waiting for viewModel to update our UI
private fun fragmentTextUpdateObserver() {
viewModel.uiTextLiveData.observe(viewLifecycleOwner, Observer { updatedText ->
binding.fragmentTextView.text = updatedText
})
}
}
Now we have a bit more code here. I tried to comment it so it's easier to follow!
First we do the view binding so we get reference to xml layout objects easier. Then we create a viewModel and after that we have the basic fragment functions (onCreateView(), onViewCreated() and onDestroyView()).
The first code we add ourself is setupClickListeners(). Here we just get a reference to the button in our UI and setup it to use a function that's in viewModel. This is because there should only be UI logic inside MainFragment. MainFragment doesn't know what data we are updating into our UI. It only cares the text gets updated when the button is clicked and viewModel is taking care of the rest of the logic.
Then we have an observer which is listening to updates that viewModel is triggering. This function will do the actual update of the UI text, but again it doesn't care what it's inserting there. It just knows it's supposed to do an update.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/fragmentTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="You have just opened a fragment!"
android:textSize="24sp"
android:padding="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/fragmentButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Update text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fragmentTextView" />
</androidx.constraintlayout.widget.ConstraintLayout>
This is just a simple xml layout which has one button and a textView.
Add MainViewModel.kt and DataModel.kt
Then we are ready to add viewModel and model!
package com.minjee.basicmvvmexample.model
// Contains the data we want to show on UI (in MainFragment)
data class DataModel(val textForUI: String)
DataModel is just a simple data container class which has a String value that we are displaying on UI after clicking the button.
package com.minjee.basicmvvmexample.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.minjee.basicmvvmexample.model.DataModel
/*
* MainViewModel
* - viewModel that updates the MainFragment (the visible UI)
* - gets the data from model
*/
class MainViewModel: ViewModel() {
// Create the model which contains data for our UI
private val model = DataModel(textForUI = "Here's the updated text!")
// Create MutableLiveData which MainFragment can subscribe to
// When this data changes, it triggers the UI to do an update
val uiTextLiveData = MutableLiveData<String>()
// Get the updated text from our model and post the value to MainFragment
fun getUpdatedText() {
val updatedText = model.textForUI
uiTextLiveData.postValue(updatedText)
}
}
So like I said earlier, viewModel is taking care of the logic that's not directly related to UI objects. First we create a DataModel so we can have a text that's going to get updated after clicking a button.
Next we add a MutableLiveData which will trigger an update for our observer that we created in MainFragment. Observer will get triggered after calling postValue() in getUpdatedText() function and then the MainFragment can update the UI text element with the updated data.
Summary
So that's it! You can find the full code and working project from my Github. I wanted to write this blog post and create the project in GitHub because it can then just be easily cloned so there's no need to write all this from scratch each time when starting a new project :) I hope you also got something out of this!
Give me a follow if you want to see more tutorials! You can also follow my Instagram whatminjaplays if you are interested to see more about my days as a software developer and a gaming enthusiast!