⇒ 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.
Components of MVVM:
- 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.
- 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.
- 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
Why Use MVVM in Android?
There are several compelling reasons to adopt the MVVM pattern in Android development:
- Separation of Concerns:
- MVVM ensures that each component has a distinct responsibility. This separation makes the codebase more organized and easier to understand.
- 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.
- 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.
- Reusability:
- The ViewModel can be reused across different views, promoting code reusability.
- 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 ourMainViewModelclass that inherits from theViewModelclass. 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 immutableLiveDataproperty. 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 thegetUserfunction, we usewithContext(Dispatchers.IO)to switch the coroutine context toDispatchers.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 newUserobject 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 thepostValuemethod. This method is thread-safe and can be called from any thread, which is why we use it here instead of thesetValuemethod, 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 namedviewModelof typeMainViewModel. This variable will be used to bind data from theMainViewModelto the UI components in the layout.
<TextView android:text="@{viewModel.userLiveData.name}" />: In thisTextView, we use data binding expression language to bind thetextattribute to thenameproperty of theuserLiveDatain theMainViewModel. 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 alateinitvariable for our binding class. Thelateinitkeyword 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 theMainViewModelusing theviewModelsdelegate, 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 theonCreatemethod, we inflate our layout using theinflatemethod of the binding class, which takes aLayoutInflateras 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. Therootproperty holds the root view in our layout file (the highest-level view in the layout).
binding.viewModel = viewModel: Here, we set theviewModelvariable in our layout file to ourMainViewModelinstance. 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 theuserLiveDataproperty in our ViewModel. Theobservemethod takes aLifecycleOwner(in this case,this, which refers to our activity) and an observer lambda, which is called whenever theuserLiveDatachanges. Inside the observer lambda, we update the text of aTextViewto display the user's name.
lifecycleScope.launch { viewModel.getUser(1) }: Finally, we uselifecycleScopeto launch a coroutine and fetch the user data when the activity is created. ThelifecycleScopeis aCoroutineScopetied 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.