Architectural Pattern_MVVM - shanjida-alam/Smart-Living-Community GitHub Wiki

Architectural Patterns

Contents

  1. 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. Why MVVM for LivSmart

    2.1. Cohesion in MVVM

    2.2. Coupling in MVVM

  3. Example MVVM Implementation Code

  4. Why Not Other Patterns?

    7.1. Why Not MVC?

    7.2. Why Not MVP?

  5. References

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.

Why We need Architectural Patterns

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.

Types of Architectural Patterns

Some well-known Architectural Patterns:

1. Model-View-Controller (MVC)

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.

2. Model-View-Presenter (MVP)

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.

3. Model-View-ViewModel (MVVM)

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.

Why MVVM for LivSmart:

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.).

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

Cohesion:

  • 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).

Coupling:

  • 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

Example MVVM Implementation Code:

1. Model Layer (Handles the data)

// 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;
    }


}
  1. 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);
    }
}
  1. 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;
        });
    }
}
  1. 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>

Why Not Other Patterns?

1. Why Not MVC (Model-View-Controller)

MVC (Model-View-Controller) is a common architectural pattern, but here’s why it may not be ideal for Liv Smart:

Tight Coupling Between Components

  • 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.

Harder to Test

  • 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.

Lack of Flexibility

  • 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.

2. Why Not MVP (Model-View-Presenter)

MVP (Model-View-Presenter) improves upon MVC, but it also has limitations:

Passive View & Overloaded Presenter

  • 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.

Less Efficient Data Binding

  • 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).

Lifecycle Management

  • 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.

References:

  1. YouTube: Introduction to MVVM Architecture Pattern
  2. GeeksForGeeks: MVVM Architecture Pattern in Android
  3. MVVM Architectural Pattern in Android
  4. ChatGPT, OpenAI, 2024.
⚠️ **GitHub.com Fallback** ⚠️