Architectural Pattern_MVVM - shanjida-alam/Smart-Living-Community GitHub Wiki
-
Architectural Patterns Overview
1.1. Why We Need Architectural Patterns
1.2. Types of Architectural Patterns
1.2.1. Model-View-Controller (MVC)
1.2.2. Model-View-Presenter (MVP)
1.2.3. Model-View-ViewModel (MVVM)
-
2.1. Cohesion in MVVM
2.2. Coupling in MVVM
-
7.1. Why Not MVC?
7.2. Why Not MVP?
An architectural pattern is like a blueprint that helps developers structure a software system in a smart and organized way. It defines the high-level structure of a system, including the arrangement of its components and how they interact with each other to meet specific business needs.
Patterns aim to balance cohesion (how closely related and focused the responsibilities of a module are) and coupling (how dependent one module is on another). Strong cohesion and loose coupling are desirable as they make the system easier to maintain and extend.
Some well-known Architectural Patterns:
Description:
- Model: Manages the data and business logic of the application. It represents the data structure and communicates with the database or other data sources.
- View: Handles the presentation of data to the user. It is the user interface (UI) part that displays data and collects user input.
- Controller: Acts as an intermediary between the Model and View. It processes user input from the View, updates the Model accordingly, and sends data back to the View.
Flow:
- User interacts with the View β Controller processes input β Model is updated β View is updated with new data.
Example:
- Web frameworks follow this pattern.
Key Benefit:
- Clear separation between UI (View) and business logic (Model), making the system more modular and testable.
Description:
- Model: Similar to MVC, it represents the data and business logic.
- View: Displays data to the user but is passive. It only calls methods provided by the Presenter and waits for data to display.
- Presenter: Acts as the middleman between Model and View, similar to the Controller in MVC, but with more control. It retrieves data from the Model, formats it, and sends it to the View. It also handles all UI logic.
Flow:
- User interacts with the View β Presenter processes the interaction and retrieves data from Model β View is updated by the Presenter.
Example:
- Android development often uses this pattern (especially in older designs) for better testability and UI separation.
Key Benefit:
- The View is very thin (dumb), with almost no logic, and the Presenter contains the logic, making it easier to test.
Description:
- Model: Represents the data and business logic, similar to MVC and MVP.
- View: Displays the data and provides user interaction, but with a key difference: it binds to the ViewModel.
- ViewModel: An abstraction of the View, holding the logic for the View. It handles user input and data presentation but does not directly reference the View. The View and ViewModel communicate using data binding, where changes in one are automatically reflected in the other.
- Repository: Acts as an intermediary between the Model and ViewModel. It is responsible for managing data operations, such as fetching data from remote or local sources, and serves as a single source of truth for the applicationβs data.
- Utils: Contains helper functions or classes that are shared across the application. These may include utilities for formatting data, managing dates and times, or handling common tasks to reduce code redundancy and improve maintainability.
Flow:
- User interacts with the View β ViewModel is updated through data binding β Repository retrieves or updates Model data β Changes in the Model are reflected in the ViewModel and automatically updated in the View.
Example:
- WPF (Windows Presentation Foundation) and Xamarin often use this pattern, as do modern Android architectures with tools.
Key Benefit:
- Automatic data binding between View and ViewModel reduces boilerplate code. The ViewModel contains no reference to the View, allowing easier unit testing. The Repository centralizes data access, enhancing maintainability, and the Util layer provides shared resources, optimizing code reuse.
MVVM (Model-View-ViewModel) is a popular architectural pattern used in Android development, particularly with modern frameworks, because it separates the business logic from the user interface (UI) to make the application easier to maintain, test, and extend.
MVVM (Model-View-ViewModel) is an ideal architectural pattern for the Liv Smart project due to its ability to separate the business logic from the user interface (UI), making the app more maintainable, scalable, and easier to test. In an application like Liv Smart, where users interact with various data sources (e.g. user accounts, notifications, etc.), this separation ensures that changes in one layer do not affect the other layers. When we say that MVVM separates business logic from the user interface (UI), it means that the parts of our application that handle the core functionality (like fetching and processing data, applying business rules) are kept completely separate from the parts that handle how things look and interact with the user (buttons, text fields, etc.).
-
Model (Business Logic): In Liv Smart, the Model layer will be responsible for managing data sources, like fetching device data from a remote server, storing user preferences, or handling notifications. This data layer is isolated from the UI, meaning any changes in the way data is fetched or stored wonβt affect the user interface.
-
View (UI): The UI in Liv Smart will display the data to the user, such as showing device statuses or user notifications, and handle user interactions (like turning devices on/off or adjusting settings). The View is purely concerned with how the data looks and doesnβt need to worry about where the data comes from.
-
ViewModel (Mediator): The ViewModel connects the Model and the View by managing the data flow and preparing it for the UI. In Liv Smart, the ViewModel will be responsible for fetching device data, transforming it into a format that can be displayed by the View, and responding to user interactions.
-
Repository (Data Management): The Repository layer centralizes all data operations for LivSmart by providing a consistent API for the ViewModel to interact with data sources. Whether fetching data from a remote server or local database, the Repository ensures a single source of truth for the applicationβs data.
-
Utils (Helper Functions): In LivSmart, Util functions will help streamline common tasks across the app, like date formatting or string manipulation, improving code readability and maintainability.
With MVVM, each component (Model, ViewModel, and View) has a single, clearly defined responsibility:
- Model: This layer is responsible only for managing the data and business logic of the application. It handles fetching, storing, and processing data (e.g., from an API, database, etc.), keeping the data-related logic cohesive and separate from other concerns.
- ViewModel: The ViewModelβs role is solely to manage the interaction between the Model and the View. It exposes data from the Model in a way the View can consume and manages the appβs UI-related state without directly interacting with the View itself.
- View: The View (often an Activity or Fragment) is responsible only for displaying the data and responding to user inputs (e.g., buttons, lists).
- Loose Coupling Between View and ViewModel: The ViewModel does not have direct knowledge of the View. The View simply observes data changes in the ViewModel (using LiveData or StateFlow), and any updates are reflected automatically in the UI. If you need to modify the UI (for instance, changing a RecyclerView layout), it doesnβt require any changes to the ViewModel or business logic. Similarly, any logic changes in the ViewModel do not impact the Viewβs structure. This loose coupling makes the system easier to modify and extend.
- Loose Coupling Between ViewModel and Model: The ViewModel retrieves data from the Model but doesnβt depend on how the data is fetched or stored. You can switch from a REST API to a local database without changing the ViewModelβs logic. This separation ensures that data-related changes (like switching data sources) donβt affect how the appβs state is managed in the ViewModel.
The app follows MVVM (Model-View-ViewModel) architecture pattern for better separation of concerns and maintainability.
com.example.smartlivingcommunity/
βββ data/
β βββ model/
| | βββEventDataModel # Data models and entities
β βββ repository/
| | βββEventRepository # Data repositories
βββ ui/
β βββ view/
β β βββ AddEventView
β β βββ MainActivity # Main content using Fragment UI components
β β βββ EventDetailsView
β β βββ EventUpdateView
β βββ viewmodel/ # ViewModels for UI components
β β βββ AddEventViewModel
β β βββ EventDetailsViewModel
β β βββ EventUpdateViewModel
βββ utils/ # Utility classes and helper functions
// EventModel.java (Model)
package com.example.createevent.data.model;
/**
* Represents an Event data model with details about an event.
* Includes information such as title, description, time, location, key, and image URL.
* This model is used for storing and retrieving event data from Firebase.
*
* @author Irtifa
*/
public class EventDataModel {
private String eventTitle; // The title of the event
private String eventDesc; // A brief description of the event
private String eventTime; // The time at which the event takes place
private String eventLocation; // The location where the event is held
private String key; // Unique key identifier for the event in the database
/**
* Default constructor for EventDataModel.
*/
public EventDataModel() {}
/**
* Parameterized constructor for creating an EventDataModel with specific details.
*
* @param eventTitle The title of the event.
* @param eventDesc The description of the event.
* @param eventTime The time of the event.
* @param eventLocation The location of the event.
*/
public EventDataModel(String eventTitle, String eventDesc, String eventTime, String eventLocation) {
this.eventTitle = eventTitle;
this.eventDesc = eventDesc;
this.eventTime = eventTime;
this.eventLocation = eventLocation;
}
// Getters and setters
/**
* Gets the unique key identifier for the event.
*
* @return The key of the event.
*/
public String getKey() {
return key;
}
/**
* Sets the unique key identifier for the event.
*
* @param key The key to set for the event.
*/
public void setKey(String key) {
this.key = key;
}
/**
* Gets the title of the event.
*
* @return The title of the event.
*/
public String getEventTitle() {
return eventTitle;
}
/**
* Gets the description of the event.
*
* @return The description of the event.
*/
public String getEventDesc() {
return eventDesc;
}
/**
* Gets the time of the event.
*
* @return The time of the event.
*/
public String getEventTime() {
return eventTime;
}
/**
* Gets the location of the event.
*
* @return The location of the event.
*/
public String getEventLocation() {
return eventLocation;
}
}
- Repository (Responsible for providing data, could be from API or database)
// EventRepository.java
package com.example.createevent.data.repository;
import androidx.annotation.NonNull;
import com.example.createevent.data.model.EventDataModel;
import com.google.firebase.firestore.CollectionReference;
import com.google.firebase.firestore.FirebaseFirestore;
/**
* Repository class for handling event data operations in Firebase.
* Provides methods for adding, updating, and deleting events from Firebase Firestore.
* This class interacts with Firebase Firestore and serves as a data layer for the application's ViewModel.
*
* @author Irtifa
*/
public class EventRepository {
private final CollectionReference eventsCollection;
/**
* Initializes the EventRepository, setting up references to the Firebase Firestore
* collection for event data storage.
*/
public EventRepository() {
FirebaseFirestore db = FirebaseFirestore.getInstance();
eventsCollection = db.collection("events");
}
/**
* Saves an event to Firestore.
*
* @param eventDataModel The event data to save.
* @param callback A callback to notify the result of the save operation.
*/
public void saveEvent(EventDataModel eventDataModel, OnEventCallback callback) {
eventsCollection.add(eventDataModel)
.addOnSuccessListener(documentReference -> callback.onCallback(true))
.addOnFailureListener(e -> callback.onCallback(false));
}
/**
* Updates an existing event in Firestore.
*
* @param key The document ID of the event to update.
* @param eventDataModel The updated event data.
* @param callback A callback to notify the result of the update operation.
*/
public void updateEvent(String key, EventDataModel eventDataModel, OnEventCallback callback) {
if (key == null || key.isEmpty()) {
callback.onCallback(false);
return;
}
eventsCollection.document(key)
.set(eventDataModel)
.addOnCompleteListener(task -> callback.onCallback(task.isSuccessful()))
.addOnFailureListener(e -> callback.onCallback(false));
}
/**
* Deletes an event from Firestore.
*
* @param key The document ID of the event to delete.
* @param callback A callback to notify the result of the delete operation.
*/
public void deleteEvent(@NonNull String key, OnEventCallback callback) {
if (key == null || key.isEmpty()) {
callback.onCallback(false);
return;
}
eventsCollection.document(key)
.delete()
.addOnSuccessListener(aVoid -> callback.onCallback(true))
.addOnFailureListener(e -> callback.onCallback(false));
}
/**
* Interface for callback to handle asynchronous event operations.
*/
public interface OnEventCallback {
void onCallback(boolean success);
}
}
- ViewModel Layer (Handles business logic and exposes data to the UI)
// AddEventViewModel.java
package com.example.createevent.ui.viewmodel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.createevent.data.model.EventDataModel;
import com.example.createevent.data.repository.EventRepository;
/**
* ViewModel for managing event data in the Add Event feature.
* This ViewModel communicates with the EventRepository for data operations
* and maintains LiveData objects for observing data changes in the UI.
* @author Irtifa
*/
public class AddEventViewModel extends ViewModel {
private final EventRepository eventRepository; // Repository for handling data operations
private final MutableLiveData<String> title = new MutableLiveData<>();
private final MutableLiveData<String> description = new MutableLiveData<>();
private final MutableLiveData<String> time = new MutableLiveData<>();
private final MutableLiveData<String> location = new MutableLiveData<>();
private final MutableLiveData<Boolean> isSaved = new MutableLiveData<>(false);
public AddEventViewModel(EventRepository eventRepository) {
this.eventRepository = new EventRepository(); // Initializes the EventRepository
}
// Getters for LiveData
public LiveData<String> getTitle() { return title; }
public LiveData<String> getDescription() { return description; }
public LiveData<String> getTime() { return time; }
public LiveData<String> getLocation() { return location; }
public LiveData<Boolean> isSaved() { return isSaved; }
// Setters for updating event data in LiveData
public void setTitle(String title) { this.title.setValue(title); }
public void setDescription(String description) { this.description.setValue(description); }
public void setTime(String time) { this.time.setValue(time); }
public void setLocation(String location) { this.location.setValue(location); }
/**
* Saves the event data using the EventRepository.
* Constructs an EventDataModel with the current field values
* and passes it to the repository for saving.
*/
public void saveEvent() {
if (title.getValue() != null && description.getValue() != null &&
time.getValue() != null && location.getValue() != null) {
EventDataModel newEvent = new EventDataModel(
title.getValue(),
description.getValue(),
time.getValue(),
location.getValue()
);
// Pass event data to repository
eventRepository.saveEvent(newEvent, success -> isSaved.setValue(success));
}
}
}
4.View Layer (Activity) (UI observing the ViewModel)
// AddEventView.java
package com.example.createevent.ui.view;
import android.os.Bundle;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.example.createevent.R;
/**
* Activity class for adding a new event.
* Handles the layout and UI setup for adding event details.
* @author Irtifa
*/
public class AddEventView extends AppCompatActivity {
/**
* Called when the activity is first created.
* Sets up the UI and applies edge-to-edge window insets handling.
*
* @param savedInstanceState If the activity is being re-initialized after previously being shut down,
* this Bundle contains the data it most recently supplied.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Enables edge-to-edge layout in this activity
EdgeToEdge.enable(this);
// Sets the content view for the activity
setContentView(R.layout.activity_add_event);
// Applies window insets to the main view to handle system bars' padding
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
}
}
- XML Layout
<!--acitivity_add_event-->
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.view.AddEventView">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
app:cardCornerRadius="30dp"
app:cardElevation="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center_horizontal"
android:padding="20dp"
android:background="@drawable/lavender_border">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Enter Event Data"
android:textSize="30sp"
android:textAlignment="center"
android:textColor="@color/lavender"/>
<EditText
android:layout_width="match_parent"
android:layout_height="60dp"
android:id="@+id/uploadEvent"
android:background="@drawable/lavender_border"
android:layout_marginTop="20dp"
android:padding="16dp"
android:hint="Enter Event name"
android:gravity="start|center_vertical"
android:textColor="@color/lavender"/>
<EditText
android:layout_width="match_parent"
android:layout_height="80dp"
android:id="@+id/uploadDesc"
android:background="@drawable/lavender_border"
android:layout_marginTop="20dp"
android:padding="16dp"
android:hint="Enter description"
android:gravity="start|center_vertical"
android:textColor="@color/lavender"/>
<EditText
android:layout_width="match_parent"
android:layout_height="60dp"
android:id="@+id/uploadTime"
android:background="@drawable/lavender_border"
android:layout_marginTop="20dp"
android:padding="16dp"
android:hint="Enter Time"
android:gravity="start|center_vertical"
android:textColor="@color/lavender"/>
<EditText
android:layout_width="match_parent"
android:layout_height="60dp"
android:id="@+id/uploadLoc"
android:background="@drawable/lavender_border"
android:layout_marginTop="20dp"
android:padding="16dp"
android:hint="Enter Location"
android:gravity="start|center_vertical"
android:textColor="@color/lavender"/>
<Button
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Add Event"
android:id="@+id/saveButton"
android:textSize="18sp"
android:layout_marginTop="20dp"
app:cornerRadius = "20dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</ScrollView>
MVC (Model-View-Controller) is a common architectural pattern, but hereβs why it may not be ideal for Liv Smart:
- In MVC, the Controller is tightly coupled to both the View and Model, making it harder to maintain and scale as the app grows. This tight coupling can become problematic for complex apps like Liv Smart.
- MVCβs Controller handles both input and business logic, which can make unit testing more difficult, especially when compared to the clean separation offered by MVVM.
- MVC doesnβt handle lifecycle events and resource management as efficiently as MVVM. For mobile apps like Liv Smart, which require proper lifecycle management, MVC would make things unnecessarily complicated.
MVP (Model-View-Presenter) improves upon MVC, but it also has limitations:
- In MVP, the Presenter does the heavy lifting by handling the UI logic, which can result in bloated and hard-to-maintain Presenters as the app grows. For an app like Liv Smart with potentially many UI components, this can lead to overly complicated code.
- Unlike MVVM, MVP requires the Presenter to manually update the View, increasing boilerplate code and reducing efficiency. MVVMβs automatic data binding is a better solution for Liv Smart, which may have frequent updates (e.g., smart device status).
- MVP lacks the inherent support for handling Android lifecycle events. In MVVM, ViewModels are lifecycle-aware, making them a better fit for Liv Smart where background processing and UI updates need to be managed efficiently.