Implementing MVVM Architecture in Android with Kotlin
In this tutorial, we will learn how to implement the MVVM (Model-View-ViewModel) architecture in Android using Kotlin. MVVM is a design pattern that helps separate the user interface (View) from the business logic (ViewModel) and the data (Model). By following this architecture, we can create cleaner and more maintainable code.
Introduction
What is MVVM architecture?
MVVM stands for Model-View-ViewModel. It is a design pattern that separates the user interface (View) from the business logic (ViewModel) and the data (Model). The Model represents the data and business logic, the View represents the user interface, and the ViewModel acts as a bridge between the Model and the View.
Advantages of using MVVM in Android
There are several advantages of using MVVM architecture in Android development. Firstly, it promotes separation of concerns, making the code more modular and easier to maintain. Secondly, it improves testability by allowing us to write unit tests for the ViewModel and the data layer. Lastly, it provides a clear separation between UI and business logic, making it easier to understand and modify the code.
Overview of Kotlin programming language
Kotlin is a modern programming language developed by JetBrains. It is fully interoperable with Java, which means we can use Kotlin and Java code together in an Android project. Kotlin offers many features that make Android development easier and more efficient, such as null safety, extension functions, coroutines, and more.
Setting up the Project
Creating a new Android project
To start implementing MVVM architecture in Android with Kotlin, we need to create a new Android project. Open Android Studio and select "Start a new Android Studio project". Follow the wizard to set up your project with the desired settings, such as package name, project name, and minimum SDK version.
Adding necessary dependencies
Once the project is created, we need to add the necessary dependencies to work with MVVM architecture. Open the project's build.gradle
file and add the following dependencies:
// ViewModel and LiveData
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
// Data Binding
implementation 'androidx.databinding:databinding-runtime:7.0.2'
kapt 'androidx.databinding:databinding-compiler:7.0.2'
Sync the project to download the dependencies.
Configuring the project for MVVM
To configure the project for MVVM architecture, we need to enable data binding in the app's build.gradle
file. Add the following lines inside the android
block:
dataBinding {
enabled = true
}
Sync the project to apply the changes.
Model Layer
Creating data models
The Model layer represents the data and business logic of the application. In this section, we will create the necessary data models for our project.
Create a new Kotlin class called "User.kt" and add the following code:
data class User(val id: Int, val name: String, val email: String)
This class represents a user with an id, name, and email. We will use this model to populate the UI later on.
Implementing repositories
Repositories are responsible for fetching data from different sources and providing it to the ViewModel. In this section, we will create a UserRepository that retrieves user data from a remote data source.
Create a new Kotlin class called "UserRepository.kt" and add the following code:
class UserRepository {
fun getUsers(): List<User> {
// Simulate fetching data from a remote API
return listOf(
User(1, "John Doe", "[email protected]"),
User(2, "Jane Smith", "[email protected]")
)
}
}
This UserRepository class has a getUsers() method that returns a list of users. In a real-world scenario, this method would make an API call to fetch the data.
Working with data sources
In this section, we will create a remote data source using a Retrofit client to fetch user data from an API.
Create a new Kotlin interface called "UserApiService.kt" and add the following code:
interface UserApiService {
@GET("users")
suspend fun getUsers(): List<User>
}
This interface defines a getUsers() method that makes a GET request to the "users" endpoint and returns a list of User objects.
Next, create a Retrofit client in a separate Kotlin file called "ApiClient.kt" and add the following code:
object ApiClient {
private const val BASE_URL = "https://api.example.com/"
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val userApiService: UserApiService by lazy {
retrofit.create(UserApiService::class.java)
}
}
This object provides a singleton instance of the Retrofit client and exposes the UserApiService.
View Layer
Creating XML layout files
The View layer represents the user interface of the application. In this section, we will create the XML layout files for our project.
Create a new XML layout file called "activity_main.xml" and add the following code:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.example.mvvmarchitecture.viewmodel.UserViewModel" />
</data>
<LinearLayout
...>
<TextView
android:id="@+id/userNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.userName}" />
<TextView
android:id="@+id/userEmailTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.userEmail}" />
<Button
android:id="@+id/loadUsersButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Load Users"
android:onClick="@{viewModel::loadUsers}" />
</LinearLayout>
</layout>
This layout file uses data binding to bind the UI elements with the ViewModel. It includes two TextViews to display the user's name and email, and a Button to load the users.
Binding views with data
To bind the views with the ViewModel, we need to create a binding object in the Activity or Fragment that inflates the layout. In this section, we will create the MainActivity that binds the views with the UserViewModel.
Create a new Kotlin class called "MainActivity.kt" and add the following code:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = viewModel
binding.lifecycleOwner = this
}
}
This MainActivity class sets the content view to the activity_main layout using data binding. It also sets the viewModel variable in the binding object and sets the lifecycle owner to this Activity.
Handling user interactions
In this section, we will add logic to handle user interactions, such as button clicks, in the ViewModel.
Create a new Kotlin class called "UserViewModel.kt" and add the following code:
class UserViewModel : ViewModel() {
private val userRepository = UserRepository()
private val _userName = MutableLiveData<String>()
private val _userEmail = MutableLiveData<String>()
val userName: LiveData<String> get() = _userName
val userEmail: LiveData<String> get() = _userEmail
fun loadUsers() {
val users = userRepository.getUsers()
_userName.value = users.first().name
_userEmail.value = users.first().email
}
}
This UserViewModel class extends the ViewModel class and includes a UserRepository instance to fetch user data. It also has MutableLiveData properties for the user's name and email, which will be observed by the View.
The loadUsers() method fetches the users from the repository and updates the MutableLiveData properties with the first user's name and email.
ViewModel Layer
Creating ViewModel classes
The ViewModel layer acts as a bridge between the View and the Model. In this section, we will create the ViewModel classes for our project.
Create a new Kotlin class called "UserViewModel.kt" and add the following code:
class UserViewModel : ViewModel() {
private val userRepository = UserRepository()
private val _userName = MutableLiveData<String>()
private val _userEmail = MutableLiveData<String>()
val userName: LiveData<String> get() = _userName
val userEmail: LiveData<String> get() = _userEmail
fun loadUsers() {
val users = userRepository.getUsers()
_userName.value = users.first().name
_userEmail.value = users.first().email
}
}
This UserViewModel class extends the ViewModel class and includes a UserRepository instance to fetch user data. It also has MutableLiveData properties for the user's name and email, which will be observed by the View.
The loadUsers() method fetches the users from the repository and updates the MutableLiveData properties with the first user's name and email.
Implementing LiveData and Observers
LiveData is a lifecycle-aware observable data holder provided by the Android Architecture Components. In this section, we will implement LiveData and observers to update the UI when the data changes.
Add the following code to the MainActivity class:
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = viewModel
binding.lifecycleOwner = this
viewModel.userName.observe(this, Observer { name ->
binding.userNameTextView.text = name
})
viewModel.userEmail.observe(this, Observer { email ->
binding.userEmailTextView.text = email
})
}
...
This code sets up observers for the userName and userEmail properties in the ViewModel. When the value of these properties changes, the observers update the corresponding TextViews in the UI.
Handling business logic
The ViewModel layer is responsible for handling the business logic of the application. In this section, we will add business logic to the UserViewModel.
Add the following code to the UserViewModel class:
fun loadUsers() {
viewModelScope.launch {
try {
val users = userRepository.getUsers()
_userName.value = users.first().name
_userEmail.value = users.first().email
} catch (e: Exception) {
// Handle error
}
}
}
This modified loadUsers() method now uses coroutines to fetch user data asynchronously. It launches a coroutine within the viewModelScope and catches any exceptions that occur during the API call.
Testing
Unit testing ViewModel
Unit testing is an important part of software development. In this section, we will write unit tests for the UserViewModel.
Create a new Kotlin test class called "UserViewModelTest.kt" and add the following code:
class UserViewModelTest {
private lateinit var userViewModel: UserViewModel
@Before
fun setup() {
userViewModel = UserViewModel()
}
@Test
fun `loadUsers should update userName and userEmail`() {
runBlocking {
userViewModel.loadUsers()
}
val userName = userViewModel.userName.value
val userEmail = userViewModel.userEmail.value
assertEquals("John Doe", userName)
assertEquals("[email protected]", userEmail)
}
}
This unit test class initializes a UserViewModel and tests the loadUsers() method. It uses runBlocking to run the test code in a coroutine and asserts that the userName and userEmail properties are updated correctly.
Instrumented testing with Espresso
Instrumented testing allows us to test the UI of our application using the Espresso testing framework. In this section, we will write an instrumented test for the MainActivity.
Create a new Kotlin test class called "MainActivityTest.kt" and add the following code:
@RunWith(AndroidJUnit4::class)
@LargeTest
class MainActivityTest {
@Rule
@JvmField
val rule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun checkUserDataDisplayed() {
onView(withId(R.id.userNameTextView))
.check(matches(withText("John Doe")))
onView(withId(R.id.userEmailTextView))
.check(matches(withText("[email protected]")))
}
@Test
fun clickLoadUsersButton_updatesUserData() {
onView(withId(R.id.loadUsersButton))
.perform(click())
onView(withId(R.id.userNameTextView))
.check(matches(withText("John Doe")))
onView(withId(R.id.userEmailTextView))
.check(matches(withText("[email protected]")))
}
}
This instrumented test class uses the ActivityScenarioRule to launch the MainActivity. The checkUserDataDisplayed() method asserts that the user's name and email are displayed correctly in the UI. The clickLoadUsersButton_updatesUserData() method performs a click on the loadUsersButton and asserts that the user's name and email are updated accordingly.
Testing data sources
In addition to testing the ViewModel and the UI, we should also test the data sources to ensure they are working correctly. In this section, we will write a unit test for the UserRepository.
Create a new Kotlin test class called "UserRepositoryTest.kt" and add the following code:
class UserRepositoryTest {
private val userRepository = UserRepository()
@Test
fun `getUsers should return a list of users`() {
val users = userRepository.getUsers()
assertEquals(2, users.size)
assertEquals("John Doe", users[0].name)
assertEquals("[email protected]", users[0].email)
assertEquals("Jane Smith", users[1].name)
assertEquals("[email protected]", users[1].email)
}
}
This unit test class initializes a UserRepository and tests the getUsers() method. It asserts that the method returns a list of users with the correct name and email.
Conclusion
In this tutorial, we have learned how to implement the MVVM architecture in Android using Kotlin. We started by setting up the project and adding the necessary dependencies. Then, we created the Model layer by defining data models, implementing repositories, and working with data sources. Next, we created the View layer by creating XML layout files, binding views with data, and handling user interactions. Finally, we implemented the ViewModel layer by creating ViewModel classes, implementing LiveData and observers, and handling business logic.
By following the MVVM architecture, we can create cleaner and more maintainable code in our Android projects. It promotes separation of concerns and improves testability. Kotlin, with its modern features and interoperability with Java, is a great choice for Android development.