MVVM , ViewModel , LiveData
Amit Nadiger
Polyglot(Rust??, Move, C++, C, Kotlin, Java) Blockchain, Polkadot, UTXO, Substrate, Sui, Aptos, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Cas, Engineering management.
The MVVM (Model-View-ViewModel) architecture is a design pattern used in software development, particularly in building user interfaces. It provides a clear separation of concerns by dividing the codebase into three main components: the Model, the View, and the ViewModel. MVVM aims to improve code organization, maintainability, and testability of applications.
MVVM is a powerful architecture for building robust and scalable applications. It promotes separation of concerns, testability, and maintainability, making it easier to develop, test, and maintain complex UI-driven applications.
Above image is copied from internet.
MVVM
Use Cases: Data-driven applications, platforms supporting data binding.MVVM, the architecture consists of three main components:
1.View (UI):?
2. ViewModel:
3. Model (Data Layer):
+-----------------+
|? ? View (UI)? ? |
+-----------------+
|? ?Data Binding? |
|? ? (XML Layout) |
+--------+--------+
? ? ? ? ?|
? ? ? ? ?|? ? ?+------------------+
? ? ? ? ?+---->|? ViewModel? ? ? ?|
? ? ? ? ? ? ? ?|? ? ? ? ? ? ? ? ? |
? ? ? ? ? ? ? ?|? Business Logic? |
? ? ? ? ? ? ? ?|? ? ? ? ? ? ? ? ? |
? ? ? ? ? ? ? ?+--------+---------+
? ? ? ? ? ? ? ? ? ? ? ? |
? ? ? ? ? ? ? ? ? ? ? ? |? ? ?+-------------+
? ? ? ? ? ? ? ? ? ? ? ? +---->|? ?Model? ? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | (Data Layer) |
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? +-------------+
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ?Services? |
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ?Database? |
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ?Network? ?|
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? +-------------+
In the above diagram:
The key aspect of MVVM is the data binding between the view and the ViewModel. The view binds its UI elements directly to the ViewModel properties, eliminating the need for manual UI updates. When the ViewModel properties change, the view automatically reflects the updates, and user interactions trigger actions in the ViewModel.
The above block diagram illustrates the flow of data and actions in the MVVM architecture, emphasizing the separation of concerns and the interaction between the components. It helps to visualize how the View, ViewModel, and Model interact with each other in an MVVM-based application.
Please note that MVVM uses a one-to-many relationship between the View and the ViewModel. i.e one view can have multiple viewModels .
Key principles and features of the MVVM architecture:
Limitation of MVVM:
Example:
data class Task(
? ? val id: Int,
? ? val title: String,
? ? val description: String,
? ? val status: String
)
class TaskRepository {
? ? // Simulated data source or API calls
? ? fun getTasks(): List<Task> {
? ? ? ? // Fetch tasks from a database or API
? ? ? ? return listOf(
? ? ? ? ? ? Task(1, "Task 1", "Description 1", "Pending"),
? ? ? ? ? ? Task(2, "Task 2", "Description 2", "Completed"),
? ? ? ? ? ? Task(3, "Task 3", "Description 3", "Pending")
? ? ? ? )
? ? }
? ? fun addTask(task: Task) {
? ? ? ? // Add task to the database or API
? ? }
? ? fun updateTaskStatus(taskId: Int, status: String) {
? ? ? ? // Update task status in the database or API
? ? }
}
2. ViewModel:
Create the viewmodel
class TaskViewModel(private val repository: TaskRepository) : ViewModel() {
? ? private val _tasks = MutableLiveData<List<Task>>()
? ? val tasks: LiveData<List<Task>> get() = _tasks
? ? init {
? ? ? ? loadTasks()
? ? }
? ? private fun loadTasks() {
? ? ? ? _tasks.value = repository.getTasks()
? ? }
? ? fun addTask(task: Task) {
? ? ? ? repository.addTask(task)
? ? ? ? loadTasks() // Refresh the task list
? ? }
? ? fun updateTaskStatus(taskId: Int, status: String) {
? ? ? ? repository.updateTaskStatus(taskId, status)
? ? ? ? loadTasks() // Refresh the task list
? ? }
}
Create a factory class that implements the ViewModelProvider.Factory interface. This factory will be responsible for creating instances of your ViewModel. For example:
class TaskViewModelFactory(private val taskRepository: TaskRepository) :
ViewModelProvider.Factory {
? ? override fun <T : ViewModel?> create(modelClass: Class<T>): T {
? ? ? ? if (modelClass.isAssignableFrom(TaskViewModel::class.java)) {
? ? ? ? ? ? return TaskViewModel(taskRepository) as T
? ? ? ? }
? ? ? ? throw IllegalArgumentException("Unknown ViewModel class")
? ? }
}
In the above example, the factory receives an instance of the TaskRepository, which is a dependency required by the TaskViewModel.
3.View (Activity):
class TaskActivity : AppCompatActivity() {
? ? private lateinit var taskViewModel: TaskViewModel
? ? override fun onCreate(savedInstanceState: Bundle?) {
? ? ? ? super.onCreate(savedInstanceState)
? ? ? ? setContentView(R.layout.activity_task)
? ? ? ? val taskRepository = TaskRepository() // Create an instance of the TaskRepository
? ? ? ? val viewModelFactory = TaskViewModelFactory(taskRepository) // Create the ViewModel factory
? ? ? ? taskViewModel = ViewModelProvider(this, viewModelFactory).get(TaskViewModel::class.java)
? ? ? ? // Retrieve the ViewModel instance using the factory and ViewModelProvider
? ? ? ? // Observe the tasks LiveData and update the UI accordingly
? ? ? ? taskViewModel.tasks.observe(this, Observer { tasks ->
? ? ? ? ? ? // Update the UI with the list of tasks
? ? ? ? ? ? // For example, update a RecyclerView adapter
? ? ? ? })
? ? ? ? // Handle user actions (e.g., button clicks) and call the ViewModel methods
? ? ? ? addButton.setOnClickListener {
? ? ? ? ? ? val newTask = Task(4, "New Task", "New Task Description", "Pending")
? ? ? ? ? ? taskViewModel.addTask(newTask)
? ? ? ? }
? ? }
}
In this example, the implementation follows the MVVM pattern with the Activity acting as the View. The ViewModel (TaskViewModel) handles the logic and provides data to the View. The View observes the LiveData property (tasks) exposed by the ViewModel and updates the UI accordingly.
Typical set of dependencies that are commonly used for implementing MVVM architecture in an Android project. Please note that these dependencies may vary depending on your specific project requirements:
dependencies {
? ? // ViewModel
? ? implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
? ??
? ? // LiveData
? ? implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
? ??
? ? // Data Binding
? ? implementation 'androidx.databinding:databinding-runtime:7.0.2'
? ??
? ? // Retrofit (for network operations)
? ? implementation 'com.squareup.retrofit2:retrofit:2.9.0'
? ? implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
? ??
? ? // Room (for local database operations)
? ? implementation 'androidx.room:room-runtime:2.4.0'
? ? kapt 'androidx.room:room-compiler:2.4.0'
? ??
? ? // Coroutines (for asynchronous programming)
? ? implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
? ??
? ? // Other dependencies
? ? implementation 'androidx.appcompat:appcompat:1.4.0'
? ? implementation 'androidx.core:core-ktx:1.7.0'
}
These dependencies include the following:
You can add these dependencies to your project's build.gradle file under the dependencies block and sync your project to make them available for use in your code. Remember to use the appropriate versions based on your project requirements and compatibility.
Note: It's important to regularly check for updates to these dependencies and use the latest stable versions to benefit from bug fixes, performance improvements, and new features.
In the above code is all the components are tightly coupled !
To achieve loose coupling and adhere to the dependency inversion principle, you can introduce interfaces and have the classes depend on those interfaces instead of concrete implementations. Here's an updated version of the code considering your suggestions:
interface ITask {
? ? fun getTasks(): List<ITask>
? ? fun addTask(task: ITask)
}
class Task : ITask {
? ? // Task implementation
}
interface ITaskRepository {
? ? fun getAllTasks(): List<ITask>
? ? fun addTask(task: ITask)
}
class TaskRepository : ITaskRepository {
? ? override fun getAllTasks(): List<ITask> {
? ? ? ? // Retrieve tasks from data source
? ? ? ? return listOf()
? ? }
? ? override fun addTask(task: ITask) {
? ? ? ? // Add task to data source
? ? }
}
class TaskViewModel(private val taskRepository: ITaskRepository) : ViewModel() {
? ? private val _tasks = MutableLiveData<List<ITask>>()
? ? val tasks: LiveData<List<ITask>> = _tasks
? ? fun loadTasks() {
? ? ? ? val allTasks = taskRepository.getAllTasks()
? ? ? ? _tasks.value = allTasks
? ? }
? ? fun addTask(task: ITask) {
? ? ? ? taskRepository.addTask(task)
? ? ? ? loadTasks()
? ? }
}
class TaskActivity : AppCompatActivity() {
? ? private lateinit var taskViewModel: TaskViewModel
? ? override fun onCreate(savedInstanceState: Bundle?) {
? ? ? ? super.onCreate(savedInstanceState)
? ? ? ? // Create the TaskViewModel by injecting the concrete implementation of ITaskRepository
? ? ? ? val taskRepository = TaskRepository()
? ? ? ? taskViewModel = ViewModelProvider(this, TaskViewModelFactory(taskRepository)).get(TaskViewModel::class.java)
? ? ? ? // Observe the tasks LiveData and update the UI accordingly
? ? ? ? taskViewModel.tasks.observe(this, { tasks ->
? ? ? ? ? ? // Update the UI with the list of tasks
? ? ? ? })
? ? ? ? // Trigger the loading of tasks
? ? ? ? taskViewModel.loadTasks()
? ? }
}
With these updates, the ITask interface represents a more abstract concept of a task, and both Task and the repository implementations work with ITask instances. The TaskViewModel now uses MutableLiveData<List<ITask>> to hold and observe the list of tasks, and the addTask() method accepts ITask as a parameter, allowing for more flexibility and loose coupling.
By introducing interfaces and coding against abstractions, you can achieve better testability and maintainability, as well as flexibility in swapping out different implementations of the interfaces as needed.
ViewModel in detail :
In Android development, the ViewModel is part of the Android Architecture Components and is designed to store and manage UI-related data in a lifecycle-aware manner. The ViewModel class is responsible for preparing and managing the data for the UI, while surviving configuration changes (such as screen rotations) and providing a clean separation between the UI and the underlying data.
Here are some key aspects and benefits of using ViewModel:
To use the ViewModel, you need to follow these steps:
领英推荐
ViewModelProvider:
The ViewModelProvider is a utility class in Android's ViewModel library that facilitates the creation and retrieval of ViewModel instances associated with a particular lifecycle owner, such as an Activity or Fragment. It is responsible for managing the lifecycle of ViewModel instances and providing the correct instance based on the lifecycle state.
The ViewModelProvider class offers several methods to obtain ViewModel instances, depending on the use case:
Once you have obtained an instance of the ViewModelProvider, you can use its get() method to retrieve the ViewModel associated with a given class:
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
In the above example, this refers to the lifecycle owner (such as an Activity or Fragment) that will be used as the key to associate the ViewModel with. The get() method retrieves the ViewModel instance of the specified class. If a ViewModel instance already exists for the given lifecycle owner, it will be returned; otherwise, a new ViewModel instance will be created.
Under the hood, the ViewModelProvider uses a ViewModelStore to store and manage the ViewModel instances.
Example that demonstrates how to use the ViewModelProvider with a custom factory to create a ViewModel instance:
class TaskViewModel(private val taskRepository: TaskRepository) : ViewModel() {
? ? // ViewModel logic and data manipulation methods go here
}
2.Create a factory class that implements the ViewModelProvider.Factory interface. This factory will be responsible for creating instances of your ViewModel. For example:
class TaskViewModelFactory(private val taskRepository: TaskRepository) : ViewModelProvider.Factory {
? ? override fun <T : ViewModel?> create(modelClass: Class<T>): T {
? ? ? ? if (modelClass.isAssignableFrom(TaskViewModel::class.java)) {
? ? ? ? ? ? return TaskViewModel(taskRepository) as T
? ? ? ? }
? ? ? ? throw IllegalArgumentException("Unknown ViewModel class")
? ? }
}
In the above example, the factory receives an instance of the TaskRepository, which is a dependency required by the TaskViewModel.
3.In your Activity class, create an instance of the TaskViewModelFactory and use it to create the ViewModel instance:
class MainActivity : AppCompatActivity() {
? ? private lateinit var taskViewModel: TaskViewModel
? ? override fun onCreate(savedInstanceState: Bundle?) {
? ? ? ? super.onCreate(savedInstanceState)
? ? ? ? setContentView(R.layout.activity_main)
? ? ? ? val taskRepository = TaskRepository() // Create an instance of the TaskRepository
? ? ? ? val viewModelFactory = TaskViewModelFactory(taskRepository) // Create the ViewModel factory
? ? ? ? taskViewModel = ViewModelProvider(this, viewModelFactory).get(TaskViewModel::class.java)
? ? ? ? // Retrieve the ViewModel instance using the factory and ViewModelProvider
? ? ? ? // Now you can use the taskViewModel to interact with your ViewModel logic
? ? }
}
In the above code, we first create an instance of the TaskRepository. Then we create an instance of the TaskViewModelFactory, passing the taskRepository as a constructor parameter. Finally, we use the ViewModelProvider to retrieve the TaskViewModel instance by passing the Activity and the factory.
By following this approach, the ViewModelProvider will handle the creation and retention of the ViewModel instance, ensuring that the same instance is retained across configuration changes. The ViewModelFactory is responsible for providing the required dependencies to the ViewModel during its creation.
Make sure to replace the TaskRepository, TaskViewModel, and MainActivity with your own classes and adjust the code according to your application's requirements.
This example demonstrates how to use the ViewModelProvider with a custom factory, which allows you to inject dependencies into your ViewModel. It provides a more flexible way to create ViewModels with complex dependencies while still benefiting from the lifecycle-aware features provided by the ViewModel architecture.
Viewmodelstore :
In Android, the ViewModelStore is a class that is responsible for storing and managing ViewModel instances. It is typically used in conjunction with the ViewModelProvider to retain ViewModel instances across configuration changes, such as screen rotations.
The ViewModelStore is associated with a specific lifecycle owner, such as an Activity or Fragment, and it ensures that the ViewModel instances are scoped to that lifecycle owner. It allows the ViewModel instances to survive configuration changes and provides a way to retrieve the same ViewModel instance when the lifecycle owner is recreated.
When you create a ViewModel instance using the ViewModelProvider, the ViewModelStore is used to store and manage that instance. The ViewModelStore keeps track of the ViewModel instances and associates them with a unique key, typically based on the lifecycle owner. This allows the ViewModelProvider to retrieve the correct ViewModel instance when requested, based on the provided key.
The ViewModelStore is internally managed by the lifecycle owner, and it gets cleared and recreated as necessary during the lifecycle of the owner. For example, when an Activity is destroyed and recreated due to a configuration change, the ViewModelStore is cleared and then populated with the ViewModel instances again, ensuring that the same ViewModel instances are available to the recreated Activity.
The ViewModelStore is an important component in the ViewModel architecture pattern because it enables the separation of concerns between the UI components (Activity or Fragment) and the ViewModel instances. It allows the ViewModel instances to hold and manage the data required by the UI components, while the UI components can focus on handling the user interface logic.
By using the ViewModelStore and ViewModelProvider together, you can ensure that the ViewModel instances are retained and associated with the appropriate lifecycle owner, making it easier to manage the state of your app's UI across configuration changes.
LiveData:
LiveData is a data holder class provided by the Android Architecture Components. It is designed to hold and emit data in a lifecycle-aware manner. LiveData is observable, which means it can be observed by other components, such as activities or fragments, to react to changes in the data.
Here are the key characteristics of LiveData:
MutableLiveData is a subclass of LiveData that allows you to modify the data it holds. It provides methods to update the value, such as setValue() and postValue(). MutableLiveData is commonly used when the data needs to be modified by the observed component, such as an activity or fragment.
Here's an example that demonstrates the usage of LiveData and MutableLiveData:
class MyViewModel : ViewModel() {
? ? private val _name = MutableLiveData<String>() // MutableLiveData to hold a string value
? ? val name: LiveData<String> = _name // Expose the LiveData as a read-only property
? ? fun updateName(newName: String) {
? ? ? ? _name.value = newName // Update the value of the MutableLiveData
? ? }
}
In the above example, we define a ViewModel called MyViewModel. It has a private MutableLiveData property called _name that holds a string value. We expose the _name property as a read-only LiveData using the name property. The updateName() function allows modifying the value of _name by calling setValue().
In an activity or fragment, you can observe the LiveData using the observe() function:
class MainActivity : AppCompatActivity() {
? ? private lateinit var viewModel: MyViewModel
? ? override fun onCreate(savedInstanceState: Bundle?) {
? ? ? ? super.onCreate(savedInstanceState)
? ? ? ? setContentView(R.layout.activity_main)
? ? ? ? viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
? ? ? ? viewModel.name.observe(this, Observer { newName ->
? ? ? ? ? ? // React to changes in the name LiveData
? ? ? ? ? ? textView.text = newName
? ? ? ? })
? ? ? ? button.setOnClickListener {
? ? ? ? ? ? viewModel.updateName("John Doe")
? ? ? ? }
? ? }
}
In the above code, we observe the name LiveData in the onCreate() method of the activity. Whenever the value of name changes, the observer's onChanged() callback will be called with the new value. In this case, we update a TextView with the new name. When the button is clicked, the updateName() function is called, which updates the value of the LiveData and triggers the observer callback.
What is Observer in the above code ?
Observer refers to an interface provided by the Android Architecture Components. It is used to observe changes in a LiveData object.
In the above code snippet, viewModel.name is an instance of LiveData, which represents a data stream that can be observed for changes. The observe() function is used to register an observer on the LiveData object. It takes two parameters: the lifecycle owner (in this case, this refers to the current activity or fragment) and an instance of the Observer interface.
The Observer interface has a single method called onChanged(), which is invoked whenever the observed LiveData emits a new value. Inside the onChanged() method, you can define the logic to react to the changes in the LiveData.
In the given example, when the name LiveData emits a new value, the onChanged() method is triggered with the new value (newName). The code inside the onChanged() block updates the textView with the new name value.
By using the Observer pattern and LiveData, you can establish a reactive relationship between the data (LiveData) and the UI components (such as TextView), ensuring that the UI always reflects the latest state of the data.
LiveData and MutableLiveData are powerful tools for implementing reactive data flows in Android applications. They provide a simple and efficient way to handle data changes and ensure that components are always observing the latest data while being aware of the lifecycle state.
Internal design of LiveData:
LiveData is designed to be a lifecycle-aware data holder that follows the observer pattern. It is built upon the concepts of observers, publishers, and lifecycle owners. Internally, LiveData utilizes several key components and mechanisms to provide its functionality.
Internal implementation of LiveData is quite complex and involves various components and mechanisms. However, I below is simplified example to demonstrate how LiveData can be implemented at a high level. Please note that this is not the exact implementation used in the Android framework, but rather a simplified version for illustrative purposes. The one in Android framework is more sophisticated. It includes additional features like lifecycle awareness, background thread handling, and more.
Here's a basic example of how LiveData can be implemented:
class LiveData<T> {
? ? private val observers = mutableListOf<Observer<T>>()
? ? private var value: T? = null
? ??
? ? fun addObserver(observer: Observer<T>) {
? ? ? ? observers.add(observer)
? ? }
? ??
? ? fun removeObserver(observer: Observer<T>) {
? ? ? ? observers.remove(observer)
? ? }
? ??
? ? fun setValue(value: T) {
? ? ? ? this.value = value
? ? ? ? notifyObservers()
? ? }
? ??
? ? private fun notifyObservers() {
? ? ? ? for (observer in observers) {
? ? ? ? ? ? observer.onChanged(value)
? ? ? ? }
? ? }
}
interface Observer<T> {
? ? fun onChanged(value: T?)
}
In this simplified implementation, LiveData is a generic class that holds a value of type T. It also maintains a list of registered observers of type Observer<T>. The addObserver() and removeObserver() methods allow adding and removing observers dynamically.
The setValue() method is responsible for updating the value of LiveData. Whenever the value changes, the notifyObservers() method is called to iterate through all the registered observers and notify them by invoking their onChanged() method.
The Observer interface defines the onChanged() method, which is called when the value of LiveData changes. Observers can implement this method to perform any necessary actions based on the new value.
Thanks for reading till end . Please comment if you have any.
Alternant Développeur éditique chez Sopra Banking Software
6 个月Hi, I was looking for a way to refresh my memory on MVVM architecture and your article appeared on my search. I'm glad I found it since it's a condensed nest of key information, so I wanted to thoroughly thank you for writing it. Can I ask though, you mentioned a potential overuse of observable, I wasn't aware of the fact that it could drastically reduce performances. Do you have an idea around how many observables it takes for that to happen ? Or in general, in what condition is usage of observables considered an overuse ?