MVVM architectural Pattern

MVVM architectural Pattern

⇒ M V V M

  • The Model-View-ViewModel (MVVM) is architectural pattern.
  • It offers a structured approach to design and develop Android applications, ensuring a clear separation of concerns and promoting maintainability, scalability, and testability.
  • In this article, we’ll dive deep into the MVVM pattern, its components, and its significance in Android development.

What is MVVM?

  • MVVM stands for Model-View-ViewModel.
  • It’s an architectural pattern that encourages a clear separation between the application’s user interface (UI), data, and logic.
  • The primary goal of MVVM is to decouple these components, making it easier to write, test, and maintain code.
notion image

Components of MVVM:

  1. Model: Represents the data and business logic of the application. It’s responsible for fetching, storing, and processing data. The model is unaware of the view and ViewModel.
  1. View: Represents the UI of the application. It displays data and sends user commands (like button clicks) to the ViewModel. The view is passive and doesn’t contain any business logic.
  1. ViewModel: Acts as a bridge between the Model and the View. It holds the presentation logic and ensures that the view displays the correct data. The ViewModel requests data from the Model, processes it, and then updates the View.
 
 

Create Directory Structure Like This

notion image
 

Why Use MVVM in Android?

There are several compelling reasons to adopt the MVVM pattern in Android development:
  1. Separation of Concerns:
      • MVVM ensures that each component has a distinct responsibility. This separation makes the codebase more organized and easier to understand.
  1. Testability:
      • Since the ViewModel doesn’t have any reference to the view or any Android-specific components, it’s easier to write unit tests for it.
  1. Data Binding:
      • Android’s Data Binding library works seamlessly with MVVM. It allows developers to bind UI components directly to data sources, reducing boilerplate code.
  1. Reusability:
      • The ViewModel can be reused across different views, promoting code reusability.
  1. Maintainability:
      • With a clear separation between components, making changes or updates to the app becomes more straightforward.
 

Benefits of MVVM over Other Architectural Patterns

While there are several architectural patterns like MVC (Model-View-Controller) and MVP (Model-View-Presenter), MVVM offers certain advantages:
  • Data Binding:
    • MVVM leverages the power of data binding, reducing the need for boilerplate code to update the UI.
  • Decoupling:
    • MVVM provides a higher degree of decoupling compared to MVC and MVP. The ViewModel doesn’t have a reference to the view, making it easier to write tests.
  • Reactivity:
    • With tools like LiveData, MVVM supports building reactive UIs that can respond to data changes in real-time
 
 

Step 1: Set Up Your Android Studio Project

First, we need to set up our Android Studio project with the necessary dependencies. In your build.gradle (Module: app) file, add the ViewModel, LiveData, and Kotlin Coroutines libraries. These libraries will help us implement the MVVM pattern and handle asynchronous tasks efficiently.
dependencies { // ViewModel and LiveData implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" implementation "androidx.fragment:fragment-ktx:1.6.1" // Kotlin Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" }
 

Step 2: Enable Data Binding

Next, enable data binding in your build.gradle (Module: app) file. Data binding allows us to bind UI components in our layouts to data sources in our app using a declarative format rather than programmatically.
android { ... buildFeatures { dataBinding = true // or // viewBinding = true } }
 

Step 3: Create the Model

Now, let’s create a data class to represent our data model. In Kotlin, a data class automatically generates utility functions such as toString()hashCode(), and equals() based on the properties defined in the class. Here, we create a User data class with id and name properties.
data class User( val id: Int, val name: String )
 

Step 4: Create the ViewModel

Next, create a ViewModel class where you will manage your app's data:
import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class MainViewModel : ViewModel() { private val _userLiveData = MutableLiveData<User>() val userLiveData: LiveData<User> get() = _userLiveData suspend fun getUser(id: Int) { // Simulate a network or database call to fetch the user withContext(Dispatchers.IO) { val user = User(id, "John Doe") _userLiveData.postValue(user) } } }
 
  • class MainViewModel : ViewModel(): Here, we define our MainViewModel class that inherits from the ViewModel class. This means that our class can take advantage of the lifecycle-aware nature of ViewModels, preserving data across configuration changes such as screen rotations.
  • private val _userLiveData = MutableLiveData<User>(): We define a private mutable live data variable to hold our user data. Being mutable means that it can be changed within the ViewModel.
  • val userLiveData: LiveData<User> get() = _userLiveData: We expose the mutable live data through a public immutable LiveData property. This ensures that the data can be observed, but not modified, from outside the ViewModel.
  • suspend fun getUser(id: Int): This is a suspending function, which means it can perform long-running operations without blocking the main thread. It is part of Kotlin's coroutines, which allow us to handle asynchronous tasks more efficiently and with less boilerplate code.
  • withContext(Dispatchers.IO): Inside the getUser function, we use withContext(Dispatchers.IO) to switch the coroutine context to Dispatchers.IO, which is optimized for I/O tasks such as network requests and database operations. This ensures that the long-running operation of fetching the user data is performed off the main thread, preventing UI freezes and ANRs (Application Not Responding errors).
  • val user = User(id, "John Doe"): Here, we simulate fetching a user from a database or network source by creating a new User object with the given ID and a hardcoded name.
  • _userLiveData.postValue(user): After fetching the user data, we update our mutable live data with the new data using the postValue method. This method is thread-safe and can be called from any thread, which is why we use it here instead of the setValue method, which must be called from the main thread.
 

Step 5: Set Up Data Binding in Your Layout

Update your layout file to use data binding. Create a layout file named activity_main.xml with the following content:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <!-- Declare ViewModel variable --> <variable name="viewModel" type="com.example.app.MainViewModel" /> </data> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/userNameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{viewModel.userLiveData.name}" /> </RelativeLayout> </layout>
  • <layout>: This is the root tag for layout files that use data binding. It indicates that this layout file will use data binding, which enables us to bind UI components in our layouts to data sources in our app using a declarative format rather than programmatically.
  • <data>: Inside the <layout> tag, we have a <data> element where we declare variables that will be used in the layout file. These variables represent the data that the layout will bind to.
  • <variable name="viewModel" type="com.example.app.MainViewModel" />: Here, we declare a variable named viewModel of type MainViewModel. This variable will be used to bind data from the MainViewModel to the UI components in the layout.
  • <TextView android:text="@{viewModel.userLiveData.name}" />: In this TextView, we use data binding expression language to bind the text attribute to the name property of the userLiveData in the MainViewModel. This is an example of one-way data binding, where data flows from the ViewModel to the UI, but not the other way around.

Step 6: Create the View

Finally, create an activity to represent the view. In this activity, you’ll observe the data in the ViewModel and update the UI accordingly:
import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.example.app.databinding.ActivityMainBinding import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // binding.viewModel = viewModel viewModel.userLiveData.observe(this, { user -> // Update UI with the user data binding.userNameTextView.text = user.name }) lifecycleScope.launch { viewModel.getUser(1) } } }
  • private lateinit var binding: ActivityMainBinding: Here, we declare a lateinit variable for our binding class. The lateinit keyword allows us to initialize the variable at a later point, and the binding class is generated automatically by the Android build system when we enable data binding.
  • private val viewModel: MainViewModel by viewModels(): We initialize the MainViewModel using the viewModels delegate, which is a Kotlin property delegate that lazily provides a ViewModel instance. This delegate is lifecycle-aware, meaning the ViewModel will be retained as long as the scope of the ViewModel (in this case, the activity) is alive.
  • binding = ActivityMainBinding.inflate(layoutInflater): In the onCreate method, we inflate our layout using the inflate method of the binding class, which takes a LayoutInflater as a parameter. This sets up the data binding for our layout.
  • setContentView(binding.root): We set the content view to the root view of our binding class. The root property holds the root view in our layout file (the highest-level view in the layout).
  • binding.viewModel = viewModel: Here, we set the viewModel variable in our layout file to our MainViewModel instance. This binds the ViewModel to the layout, allowing us to use ViewModel properties directly in the XML layout.
  • viewModel.userLiveData.observe(this, { user -> binding.userNameTextView.text = user.name }) We observe the userLiveData property in our ViewModel. The observe method takes a LifecycleOwner (in this case, this, which refers to our activity) and an observer lambda, which is called whenever the userLiveData changes. Inside the observer lambda, we update the text of a TextView to display the user's name.
  • lifecycleScope.launch { viewModel.getUser(1) }: Finally, we use lifecycleScope to launch a coroutine and fetch the user data when the activity is created. The lifecycleScope is a CoroutineScope tied to the lifecycle of the activity, meaning any coroutines launched in this scope will be automatically canceled when the activity is destroyed.
And that’s it, you’ve successfully implemented a basic MVVM example. You can now build the app and it should show the text that we set in our viewmodel.