Sprint4 - fedemelo/unitrade-wiki GitHub Wiki

Sprint 4: Deliverable

This page gathers all deliverables expected for Sprint 4.

As in Sprint 3, the numbering of the sections in this document follows the numbering of the sections in the deliverable. However, the order of the sections differs.

1, 4 & 7.a. Delivered Features

This section lists all the features in the app, detailing which were implemented in Sprint 2, Sprint 3, and Sprint 4.

Subsections are included to distinguish between features from different sprints.

1 & 7.a. Features from Sprint 2

Feature Description Views in which it was implemented
Microsoft Login (University Restricted) Allows only students from the University of the Andes to log in using their Microsoft accounts. Login View
Preferences Setup Upon first login, users select their product preferences (e.g., sports, lab equipment) to personalize their experience. Preferences Setup View
Home View with Product Listings Displays all products available for sale or rent, providing a central hub for browsing items. Home View
Text Search Functionality Enables users to search for products using a search bar, making it easier to find specific items. Home View
Price Range Filter Allows users to filter products based on a specific price range to match their budget constraints. Home View
Sort Products Feature Users can sort products by rating or price in ascending or descending order to organize listings according to their preferences. Home View
Category Selection Users can select specific categories to view all products within that category, streamlining the browsing experience. Home View
"For You" Personalized Category Displays products based on the user's interests selected during sign-up, offering a tailored shopping experience. Home View
Upload Product for Sale Allows users to list a product for sale by entering details like name, description, price, condition, and uploading an image from the gallery or camera. Upload Product View
Upload Product for Rent Enables users to list a product for rent, including all sale details plus specifying the rental period in days. Upload Product View
Image Upload Capability Users can upload images of their products either from their device gallery or by taking a new photo, enhancing the appeal of their listings. Upload Product View
AI Category Assignment Utilizes an AI model to automatically assign relevant categories to uploaded products, improving searchability and organization. Upload Product View (Background Process)
Context-Aware Theme Switching Offers a time-aware mode that automatically switches between light and dark themes based on the time of day (light mode during the day, dark mode at night). Profile View (Theme Settings)

1 & 7.a. Features from Sprint 3

Feature Description Views in which it was implemented
Semester and Major Input Users input their semester and major, which can be used for analysis and potentially for further user personalization. Preferences Setup View
Profile Viewing Users can access their profile to view and edit personal preferences. Profile View
Light/Dark Mode Toggle Users can manually switch between light and dark themes to suit their visual comfort preferences. Theme Settings
View User Listings Users can view all the products they have listed, both for sale and rent, in one place. My Listings
Listing Popularity Metrics Shows users how many others have saved their listed products, providing insights into the popularity and interest in their items. My Listings

Post-Feedback Changes

The following updates were made to the app based on feedback received during Sprint 3:

  • All red font colors, present in many of the eventual connectivity messages, were changed to black for better readability.
  • The login flow was changed to favor users maintaining their session after closing the app, instead of having to choose an account every time they open it.

4. New Features: Sprint 4

The following new features were implemented in Sprint 4. They are explained in the following table.

Notice that each feature was implemented for both Swift and Flutter, which means that the number of features equals double the number of rows in the table.

Feature Description View in which it was implemented
No Internet Banner A banner is displayed when the app detects no internet connection, informing the user of the issue. The banner disappears once the connection is reestablished. All
Save Favorites Users can save products to a "Favorites" list for easy access and tracking of items they are interested in. Home View
My Favorites View A new view was created to display all products saved by the user in their "Favorites" list. My Favorites
Product Detail View Users can view detailed information about a product, including its image, name, description, price, condition, rent period (if applicable) and whether it is up for sale or for rent. Detail
Buy Now / Rent Now Buttons Users can directly purchase or rent a product from the Product Detail View, which can be accessed either from the Home View or from the Favorites view, streamlining the process. Detail
My Orders View A new view was created to display all products the user has purchased or rented, providing a centralized location for tracking their orders. My Orders

2. Value Proposition

UniTrade is a marketplace app made for university students. It solves their problems of finding and exchanging academic materials quickly and easily. The app uses features like AI-powered product categories and personalized recommendations to save users time and effort. For example, the "For You" section, added in Sprint 2, shows products based on each student’s preferences, making it easier to find what they need. Features from Sprint 4, like saving "Favorites" and quick "Buy Now" buttons, make shopping faster and simpler. The app also works well on both Android and iOS, thanks to its development in Swift and Flutter.

UniTrade makes money through a smart mix of charging small fees on transactions (5%-10%), offering premium features, and showing ads that match student interests. Business questions from Sprint 3 and Sprint 4, such as analyzing where users shop (Favorites or Home View) and studying purchase habits by semester, help improve these strategies. For example, knowing which semester students are most likely to buy helps target ads better. By collecting and analyzing data from features like the "Favorites" list, the app gets smarter over time, giving students a better experience while also helping UniTrade grow as a business.

3. Micro-optimization Strategies

Swift App Micro-optimizations

  • Reuse Created Categories Instead of Recreating Them

The first micro-optimization identified in the Swift application is related to the creation of unnecessary objects. Although this does not occur inside a loop, the identified method is frequently called, resulting in redundant object creation. The culprit in this case is the createCategories() method:

private func createCategories() -> [Category] {
    let forYouCount = viewModel.forYouCategories.count
    let forYouCategory = Category(name: "For You", itemCount: forYouCount, systemImage: "star.circle")
    
    return [
        forYouCategory,
        Category(name: "Study", itemCount: 43, systemImage: "book"),
        Category(name: "Tech", itemCount: 57, systemImage: "desktopcomputer"),
        Category(name: "Creative", itemCount: 96, systemImage: "paintbrush"),
        Category(name: "Lab", itemCount: 30, systemImage: "testtube.2"),
        Category(name: "Personal", itemCount: 78, systemImage: "backpack"),
        Category(name: "Others", itemCount: 120, systemImage: "sportscourt")
    ]
}

This method is responsible for generating a list of categories displayed in the ExplorerView. Each time it is called, it creates a new array of Category objects, even though the data often remains static during the lifecycle of the view. To optimize this, the createCategories() method was replaced by a computed property that calculates the categories only when needed and avoids recreating them unnecessarily.

The optimized computed property looks like this:

private var categories: [Category] {
    let forYouCount = viewModel.forYouCategories.count
    let forYouCategory = Category(name: "For You", itemCount: forYouCount, systemImage: "star.circle")
    
    return [
        forYouCategory,
        Category(name: "Study", itemCount: 43, systemImage: "book"),
        Category(name: "Tech", itemCount: 57, systemImage: "desktopcomputer"),
        Category(name: "Creative", itemCount: 96, systemImage: "paintbrush"),
        Category(name: "Lab", itemCount: 30, systemImage: "testtube.2"),
        Category(name: "Personal", itemCount: 78, systemImage: "backpack"),
        Category(name: "Others", itemCount: 120, systemImage: "sportscourt")
    ]
}

This computed property avoids recreating the object unnecessarily by ensuring that the categories array is only calculated when accessed. The view then references this property instead of repeatedly invoking a method.

The final optimized implementation in the ExplorerView looks like this:

CategoryScroll(
    selectedCategory: $viewModel.selectedCategory,
    categories: categories
)

Instead of calling createCategories() multiple times, the view now accesses the categories computed property.

Before this micro-optimization, every interaction that required accessing categories, such as scrolling or refreshing the view, triggered the creation of a new array of Category objects. By implementing the computed property, this overhead was removed, leading to improved CPU performance.

The following graph shows the CPU usage before the micro-optimization, where the createCategories() method was called multiple times during user interactions. Tha graph shown corresponds to the user entering the home screen, where the categories are, and then switching between that screen and another three times:

Bildschirmfoto 2024-12-02 um 9 58 35 PM

The following graph shows the memory usage after implementing the computed property. As before, the test consisted of the user entering the home screen and switching between that screen and another three times:

Bildschirmfoto 2024-12-02 um 10 00 10 PM

Naturally, the graphs have similar behaviors as they represent the same user interactions. The graph that corresponds to the main thread CPU usage is also included, as all of the operations happen on the main thread. It is important to notice that the difference between the two graphs is the scale in which the peaks happen. In the first graph, that corresponds to the operation without the micro-optimization, the peaks are higher, with the first one piking up to 192% CPU usage, while in the second graph, that corresponds to the operation with the micro-optimization, the peaks are lower, with the first one piking up to 152% CPU usage and the second peak being proportionally lower (and, therefore, lower than its corresponding peak on the first graph). This shows that the micro-optimization helped streamline the performance of the ExplorerView.

  • Avoid Frequent Date Formatter Creation in Method

The second micro optimization identified in the Swift application is related to the creation of unnecessary objects. In this case, the unnecessary object creation does not happen inside a loop but rather inside a method that is called multiple times. The method that was identified as the culprit is the following:

private func updateFirebase(for product: Product, isPurchase: Bool) {
    guard let userId = Auth.auth().currentUser?.uid else {
        showAlert(message: "User is not authenticated.")
        return
    }
    
    let documentRef = firestore.collection("products").document(product.id)
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd"
    let currentDate = dateFormatter.string(from: Date())
    
    firestore.collection("users").document(userId).getDocument { [weak self] (document, error) in
        var buyerSemester: String? = nil
        
        if let document = document, document.exists {
            let data = document.data()
            buyerSemester = data?["semester"] as? String
        } else if let error = error {
            self?.showAlert(message: "Failed to retrieve user data: \(error.localizedDescription)")
        } else {
            self?.showAlert(message: "User data not found.")
        }
        
        documentRef.updateData([
            "in_stock": false,
            "buyer_id": userId,
            "buyer_semester": buyerSemester ?? NSNull(),
            "purchase_date": currentDate,
            "purchase_screen": purchaseScreen
        ]) { error in
            if let error = error {
                self?.showAlert(message: "Failed to update product: \(error.localizedDescription)")
            } else {
                let successMessage = isPurchase
                ? "Product purchased successfully!"
                : "Product rented successfully!"
                self?.showAlert(message: successMessage)
            }
        }
    }
}

The method above has the responsibility of updating a product in the Firebase Firestore database. The method is called every time a user buys or rents a product. The problem with this method is that it creates a new DateFormatter object every time it is called. This is unnecessary because the DateFormatter object is not dependent on any other object in the method. A solution to this problem is to create the DateFormatter object as a class variable and use it in the method. However, an even better solution And the one that was implemented was to create a lazy property to avoid repeated creation.

The lazy property that was created is shown below:

private lazy var dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    return formatter
}()

The code snippet below shows the final optimized version of the method, where the object is not created each time the method is called but rather is created only once when the class is instantiated.

private func updateFirebase(for product: Product, isPurchase: Bool) {
    guard let userId = Auth.auth().currentUser?.uid else {
        showAlert(message: "User is not authenticated.")
        return
    }
    
    let documentRef = firestore.collection("products").document(product.id)
    let currentDate = dateFormatter.string(from: Date())
    
    firestore.collection("users").document(userId).getDocument { [weak self] (document, error) in
        var buyerSemester: String? = nil
        
        if let document = document, document.exists {
            let data = document.data()
            buyerSemester = data?["semester"] as? String
        } else if let error = error {
            self?.showAlert(message: "Failed to retrieve user data: \(error.localizedDescription)")
        } else {
            self?.showAlert(message: "User data not found.")
        }
        
        documentRef.updateData([
            "in_stock": false,
            "buyer_id": userId,
            "buyer_semester": buyerSemester ?? NSNull(),
            "purchase_date": currentDate,
            "purchase_screen": "home"
        ]) { error in
            if let error = error {
                self?.showAlert(message: "Failed to update product: \(error.localizedDescription)")
            } else {
                let successMessage = isPurchase
                ? "Product purchased successfully!"
                : "Product rented successfully!"
                self?.showAlert(message: successMessage)
            }
        }
    }
}

The micro-optimization surprisingly was able to substantially optimize the memory usage of the application when buying successive products. This can be attributed to the fact that the object is now created once instead of being created for each product upload.

The following graph shows the memory usage when three different products are bought before the micro-optimization was implemented:

Bildschirmfoto 2024-12-02 um 9 31 59 PM

The following graph shows the memory usage when three different products are bought after the micro-optimization was implemented:

Bildschirmfoto 2024-12-02 um 9 33 46 PM

Flutter App Micro-optimizations

For the first micro-optimization, we decided to take a look into the package that the application is currently using for checking whether the device has connection or not (connection meaning either wifi or mobile data). Previously, our application was using connectivity_plus package in order to constantly monitor internet connection changes. However, we noticed during our development process that this service sometimes taking some time before actually notifying changes in the connection status of the devices. We did some tests inside the app to see how the package was performing and when toggling on/off the mobile data for the phone we got the following results:

image

This means that when the mobile phone experiences some sort of change in the connection status, the app takes about 10 seconds before it registers the change. This value should be taken with a grain of salt, because the process corresponds to an operation done by another package it did not appear in the CPU profiler as a process, because of this we had to manually record the start/end time of the process using Flutter logging tools which means that the raw duration of getting the internet status is lower than 10 seconds. Either way, we opted for a different connectivity package called internet_connection_checker_plus which is faster and lighter flutter package built on top of our previously used connectivity_plus package. When executing the same tests in the flutter app but with the new package we got the following results:

image

Same as before, these values are higher than what the raw connection status checking time actually is but it does show a mayor improvement in performance. Furthermore, when using the app we found that new package consistently exhibited faster response times when detecting changes in internet connectivity. As before, the recorded values include overhead, but the relative improvement in performance is clear.

We also did a number of micro-optimizations in different parts of the application; a list of this micro-optimizations is presented below:

  • Avoid Creating Unnecessary Objects

In components where an object was instantiated multiple times, we instead created a single instance as a member variable of the corresponding class to repeat object creation. The following snippet illustrates the code before the micro-optimization

  MyOrdersViewmodel() {
    fetchProducts();
  }

  Future<void> fetchProducts() async {
    isLoading = true;
    isOfflineData = false;
    notifyListeners();
    try {
      var connectivity = ConnectivityService();
      hasConnection = await connectivity.checkConnectivity();

      if (hasConnection) {
        await _fetchProductsFromFirebase();
      } else {
        isOfflineData = true;
        await fetchProductsFromHive();
      }
    } catch (e) {
      print("Error fetching products: $e");
    } finally {
      isLoading = false;
      notifyListeners();
    }
  }

The problem in this case is that the object is instantiated inside the fetchProducts method every time it is called. Instead, what we can do is just to create a single instance of the ConnectivityService object and use it inside the method. Here is the code snippet after micro-optimization:

  final ConnectivityService _connectivityService = ConnectivityService();
  MyOrdersViewmodel() {
    fetchProducts();
  }

  Future<void> fetchProducts() async {
    isLoading = true;
    isOfflineData = false;
    notifyListeners();
    try {
      hasConnection = await _connectivityService.checkConnectivity();

      if (hasConnection) {
        await _fetchProductsFromFirebase();
      } else {
        isOfflineData = true;
        await fetchProductsFromHive();
      }
    } catch (e) {
      print("Error fetching products: $e");
    } finally {
      isLoading = false;
      notifyListeners();
    }
  }
  • Use Indexed Loops Instead of for-each Loops

In the application we found some instances in which we iterated over an object, an example of this is when we are saving products in local storage in which case we use a for-each loop to iterate over the array and save each product, the code snippet for this is shown below:

final box = await Hive.openBox<ProductModel>('myOrders');
await box.clear();
for (var product in products) {
  box.put(product.id, product);
}

Instead, we changed this loop to a indexed loop which is the recommended implementation for better performance in memory-intensive scenarios. The code snippet for the new implementation goes as follows:

for (int i = 0; i < products.length; i++) {
  final product = products[i];
  box.put(product.id, product);
}
  • Use Transparent Backgrounds

Although we did not find any overdraw In the application as every screen was in X1 Overdraw (Blue color) we still discovered some instances in which containers and other widgets were painting the background with the same color as the surface, we decided to change the background color the containers to transparent in order to avoid instances that may cause overdrawing. The change is as follows:

color: Theme.of(context).colorScheme.surface, changes to color: Colors.transparent,

-- Use Lifecycle Methods Properly to Avoid Memory Leaks

In files where functionalities such as local storage with Hive are used and Boxes are opened, it is important to take care of the life cycle of the objects that help use handle local storage. Because of this, it is important to add a way to dispose of these objects in the appropriate way. The dispose() method was overridden in the classes were it is necessary. The code snippet added is shown below:

@override
void dispose() {
  Hive.close();
  super.dispose();
}

Now, since all of the changes made are such small changes it was not worth comparing what the profiler shows for each of the changes. Instead, we navigate through the application before the changes and compare to the same navigation process after the micro-optimizations are made. First off, here we can see some of the data the profiler shows before the micro-optimizations:

  • App performance and GUI rendering

image

  • Build time when changing views

image

  • Memory allocation in the application

image

  • Memory chart when navigating the app Home view

image

Now when comparing to after we made the micro-optimizations we see the following results:

  • App performance and GUI rendering

image

  • Time for build when changing views

image

  • Memory allocation in the application

image

  • Memory chart when navigating the app Home view

image

All in all, we see slight improvements in UI build time for some of the slow frames of the application which go down from 28ms to just a little more than the 16ms rendering threshold. We also see improvements in the total size of the memory being used by the app going down by 8MB as well as reducing instances of certain classes most notably ProductModel. In terms of the memory chart there is also a slight improvement as we see a more stable line for the RSS which indicates the total memory consumed by the app.

5. View Catalogue

The following table lists all the views in the app. The features available in each view have been detailed in the Delivered Features section above.

Notice that, obviously, each view is implemented in both the Swift and Flutter apps.

View implemented in Sprint 4 are flagged.

Name Sprint 4?
Login View
Preferences Setup View
Home View
Upload Product View
Profile View
Theme Settings
My Listings
My Favorites X
My Orders X
Detail X

6 & 7b. Business Questions

The following table contains the business questions that were implemented for this sprint, with their respective type, rationale, and the view in which they were implemented.

Warning

Not all of the following business questions are a subset of those presented in the Sprint 1 Deliverable. Notice that only 14 business questions were defined in the Sprint 1 Deliverable: 10 for MS4 and 4 more for the final deliverable. Out of the 14 business questions defined in the Sprint 1 Deliverable, 6 were implemented in the Sprint 2 Deliverable, and 6 more were implemented in the Sprint 3 Deliverable, leaving only 2 business questions left to be implemented in the Sprint 4 Deliverable. Consequently, four of the following business questions are new and were not defined in the Sprint 1 Deliverable.

Business question Type Rationale View in which it was implemented
1 What proportion of users buy or rent from the Home View versus the My Favorites view? Type 3 This helps the business understand where users are making purchasing decisions and optimize both views accordingly. Analytics Dashboard
2 Is the average response time for buying or renting an item in the marketplace, in Android and iOS, below the defined objetive threshold of 1 second Type 1 Ensuring a fast response time is crucial for user satisfaction and retention. Analytics Dashboard
3 Which user demographic by semester is most likely to buy or rent materials? Type 4 Understanding purchase or rental behavior by semester aids in targeted marketing and partnerships with third-party companies. Analytics Dashboard
4 Is the last time that the user bought or rented a product within the expected range of 30 days? Type 2 If users are not buying or renting products within the expected timeframe, it could indicate a lack of interest or issues with the app or the products offered in it. My Orders
5 Does the average number of favorites per user exceed our maximum expected limit of 6 favorites? Type 3 If users are exceeding the favorites limit, it could indicate the need for design adjustments or introducing a filtering mechanism. Analytics Dashboard
6 Which items has the user saved to My Favorites for future consideration? Type * (2 and 3) Helps evaluate the user’s behavior and preferences, which in turn may lead to modify My Favorites view (e.g., adding categories or folders) or determine whether the feature is helpful. My Favorites

4.a & 7.e. Multi-threading Strategy for Sprint 4

Flutter

In the Flutter app, we implemented a new class similar to the previous multithreading solution. We created the class favorites_service.dart that is designed as a singleton to petition the favorite product data using a secondary thread, this helps the performance by giving this task to a separate isolate. It can only have one instance of the class due to the private constructor, providing a centralized way to load and access product data across the app. It starts with the function loadFavoritesInBackground that spawns a new isolate to fetch the data from Firebase, when the user goes to the favorites screen, it checks the thread if the products are loaded with the line FavoritesService.instance.favoriteProducts. This helps the app performance because the process of retrieving products is a costly one. Due to the storage of the userID this fetch can be executed at the start of the app.

Swift

In the Swift app, we implemented a similar multithreading strategy to enhance performance as the one for the last Sprint. A new method, fetchFavoritesInBackground, was added to the ExplorerViewModel to handle data fetching using a secondary thread. This method leverages DispatchQueue.global with a quality-of-service level set to .userInitiated, ensuring priority for tasks critical to user experience. Initially, it loads the "IDs" for all favorites of an user in the background, updating the isFavorite of each of these producta upon success. Simultaneously, products are fetched asynchronously from Firestore with the method loadProductsFromFirestore. This approach centralizes data loading while maintaining UI responsiveness, marking isDataLoaded as true once both processes (products and favorites ids) completes.

4.b & 7.d. Local Storage Strategy for Sprint 4

Flutter

For the Flutter application, we will use Hive as a NoSQL database to store products that will be shown when the user has no internet connection. This implementation will be done mainly for the MyOrders view in which the user can see all of the products he has rented/bought over time. Hive uses a concept called boxes which is basically a data storage unite which could be equivalent to a table in a traditional database. For this strategy we have created a new box in which we save and extract the products by products id. It uses key-value pair storage and also allows for persistent storage and uses thread-safe mechanisms, making them suitable for background operations. This implementation is useful as it allows us to show the user information even when no connection is available, and maintain a smooth flow throughout the app.

Swift

For the Swift application, the MyOrdersView implements a local storage strategy using UserDefaults to ensure a seamless user experience, even in offline scenarios. When the user has an internet connection, the app retrieves the latest order data from Firestore and saves it in UserDefaults for future access. If the user is offline, the app automatically retrieves and displays the orders stored in UserDefaults, ensuring data availability regardless of connectivity.

4.c. & 7.f. Caching Strategy for Sprint 4

Note

Notice this strategy is not implemented on a new feature. This was allowed by Professor Vivian.

Flutter

For the flutter application we used a library for storage and caching of images, we used especially cached_network_image, so the images from the network are stored in a local directory for easy access and rendering. This change helps the app to load and run smoothly, because it doesn't need to fetch multiple times the same image from the network every time we load the same screen, also, it helps different screens that access the same image, for example after loading the home page if the user enters the detail view of a product the image is already loaded in the cache so it doesn't need to make another request for the image. Inside the app we only need to change all the Image.network to CachedNetworkImage, we can also personalize the placeholder while it loads and an error widget if the request failed.

Swift

For the Swift application, we implemented a caching strategy to improve performance and reduce network usage by caching images of products. We utilized the Kingfisher library, which provides an efficient way to download and cache images from the network. Kingfisher stores images locally in a cache directory, ensuring that the app doesn't repeatedly fetch the same image from the network when navigating across screens. This significantly enhances the app's responsiveness. For example, if a product's image is loaded on the home screen, it will already be available in the cache when the user navigates to the product detail view, eliminating the need for additional network requests. In our implementation, we replaced direct calls to UIImageView with Kingfisher's kf.setImage method, which handles downloading, caching, and displaying the image seamlessly. Additionally, placeholders can be customized to show while the image loads, and fallback images are displayed in case of a failed request. This approach aligns with the goals of Flutter's caching strategy, ensuring a smooth user experience and optimized performance.

4.d. & 7.c. Eventual Connectivity Strategies

Every single view in the application was implemented with an eventual connectivity strategy. For all existing views, eventual connectivity strategies have been described in previous deliverables. The following table summarizes the eventual connectivity strategy for each new view implemented in Sprint 4.

Name Strategies Sprint 4?
Overall In all the screens of the aplication a banner was added to inform the user when there is no internet connection. This banner is shown when the app detects no internet connection and is shown above the bottom navigation bar, with a clear text and a no cknection icon. The banner disappears once the connection is reestablished. X
My Orders The eventual connectivity strategy that was followed for this view was network falling back to local storage. The application will first check if connection is available, if so it will request the users bought/rented products to display and also save them in local storage. In case no connection is available, it will then search local storage for previously saved products and display them instead indicating that the information might be outdated. In case there is nothing on local storage it will just display a message indicating that no connection is available, however this case is a rare case that the user will usually not encounter. X
Detail The eventual connectivity strategy for this view ensures that all actions related to BUY NOW and RENT NOW work seamlessly under connectivity constraints: (1) If the user is offline and tries to perform these actions, a pop-up informs them of the lack of internet. That is, users are not allowed to buy or rent products without an internet connection, as that could potentially block other users from buying or renting the same product. (2) If the user is online, the app will proceed with the purchase or rental as usual. X
My Favorites The eventual connectivity strategy for this view involves displaying a No Internet Connection screen with an icon when the user attempts to access it without internet. Once connectivity is restored, the favorite items are fetched and displayed. This approach ensures clarity for the user while avoiding confusion with outdated or unavailable data. Favorite products where not saved on local storage, as they are not critical information and the user can wait for the connection to be restored. X

Ethics Video

Video.

⚠️ **GitHub.com Fallback** ⚠️