Sprint 3 - MOVILES-G22-2025/Wiki GitHub Wiki
- User authentication (sign in and sign up) requesting Uniandes email and additional students data.
- Add a product for sale with its respective information using the phone's camera sensor and accessing the photo gallery.
- List of available products with their respective details (name, description, images and seller).
- Personalized filter recommendations by category based on user searches and clicks.
- Efficient product search based on product name and description.
- Personalized notifications when a user's favorite product drops in price.
- Product recommendations during key periods of university.
- Profile page with routes to favorite products and products published by the user with the option to edit or delete that product.
- List of the user's favorite products.
- Chat page for communication between seller and buyer of a product.
- Personalized notifications to inform the user about updates on active listings.
No. | Question | Implemented by |
---|---|---|
1 | How many users create an account and log in per week? | Already implemented in Sprint 2 |
2 | What are the busy hours for the marketplace usage? | Already implemented in Sprint 2 |
No. | Question | Implemented by |
---|---|---|
1 | Which filters should be suggested to the user based on their past searches? | Already implemented in Sprint 2 |
2 | Has a saved item’s price dropped or has a new listing appeared for a saved search? | Already implemented in Sprint 2 |
3 | Are there new messages or updates on an active listing? | Sara Benavides Mora |
No. | Question | Implemented by |
---|---|---|
1 | Which features are used less than twice a month on average? | Luisa Hernández |
2 | Which product categories receive the highest user engagement in terms of searches, listings, and transactions? | Already implemented in Sprint 2 |
3 | What percentage of users enable push notifications, and do they interact with them? | Already implemented in Sprint 2 |
4 | How frequently do users use the in-app messaging feature, and does it impact transaction completion rates? | Daniela Castrillón |
No. | Question | Implemented by |
---|---|---|
1 | What types of products are most frequently favorited by students from specific academic programs? | Juan Jose Cediel |
2 | At what times during the semester do students from a specific program create the most academic products | Mateo Calderón |
The second Type 4 business question was changed since the one defined in Sprint 1 was not convenient since we have only one category per product.
No. | Question | Implemented by |
---|---|---|
1 | Which features are used less than twice a month on average? | Luisa Hernández |
Option | Used | Purpose |
---|---|---|
a. SQLite | ✅ | Save structured data such as products, drafts, and user information for offline availability. |
b. Hive | ✅ | Store lightweight data like favorite products and featured categories for fast local access. |
c. Flutter Secure Storage | ✅ | Securely store sensitive data such as user authentication credentials. |
# | Strategy |
---|---|
1 | Cache, falling back to network |
2 | Network only |
3 | Network falling back to cache |
4 | Cached on network response |
5 | Cache then network |
6 | Generic fallback |
Id connectivity scenario | 1 |
---|---|
Event description | The user attempts to mark a product as favorite while offline. The action is accepted locally, but no immediate sync is possible with the server. |
System response | Store the product ID locally as a pending favorite action. When connectivity is restored, synchronize with Firestore and update both the product and user favorite lists. |
Possible Antipatterns | 2, 5 |
Caching + Retrieving strategy | Strategy 2 and 4 from the team's list |
Storage Type | 3.b – Local NoSQL (Hive) |
Stored data type | Key-value <userId, List<productId>>
|
Id connectivity scenario | 2 |
---|---|
Event description | The user is filling out the product listing form (name, description, price, images) but the app closes unexpectedly due to a crash or forced closure. |
System response | Automatically save the partial product data locally as a draft, including any images and entered fields, so the user can recover it later in the "My Drafts" section without losing progress. |
Possible Antipatterns | 1, 6 |
Caching + Retrieving strategy | Strategy 4 and 3 from the team's list |
Storage Type | 3.a – Local SQL database (SQLite with sqflite) |
Stored data type | Object structure <DraftProduct> (fields: name, description, category, price, imagePaths) |
Id connectivity scenario | 3 |
---|---|
Event description | The user completes the entire product listing form (including name, description, price, category, and images) while offline. |
System response | The product is saved locally as a pending publication. Once an internet connection is detected, the app automatically uploads the product data and associated images to Firebase without requiring additional user action. |
Possible Antipatterns | 1, 3, 6 |
Caching + Retrieving strategy | Strategy 4 and 2 from the team's list |
Storage Type | 3.a – Local SQL database (SQLite with sqflite) for metadata + local storage for image files |
Stored data type | Object structure <PendingProduct> (fields: name, description, category, price, imagePaths, syncStatus) |
Id connectivity scenario | 4 |
---|---|
Event description | The user updates their profile information (e.g., name, major, semester) while offline. |
System response | The app saves the updated user data locally. When connectivity is restored, the changes are automatically synchronized with Firestore, updating the user's document in the database. |
Possible Antipatterns | 2, 5, 7 |
Caching + Retrieving strategy | Strategy 3 from the team's list |
Storage Type | 3.a – Structured local database (SQLite for Flutter) |
Stored data type | Updated user profile fields pending synchronization (pendingProfileUpdates ) |
Id connectivity scenario | 5 |
---|---|
Event description | The user attempts to delete a product they have uploaded while offline. |
System response | The app marks the product for deletion locally. When the device reconnects to the internet, the pending deletion is synchronized with Firestore, and the product is removed from the database. |
Possible Antipatterns | 2, 5, 8 |
Caching + Retrieving strategy | Strategy 2 from the team's list |
Storage Type | 3.a – Structured local database (SQLite for Flutter) |
Stored data type | Product IDs queued for deletion (pendingDeletions ) |
Id connectivity scenario | 6 |
---|---|
Event description | The app dynamically shows featured categories every hour based on user clicks. If the user is offline, the last synchronized featured categories must still be displayed. |
System response | Retrieve the latest cached featured categories stored locally in Hive. When the connection is restored, fetch updated data from Firestore and refresh the local cache. |
Possible Antipatterns | 1, 4 |
Caching + Retrieving strategy | Strategy 5 and 3 from the team's list |
Storage Type | 3.b – Local NoSQL database (Hive) |
Stored data type | Key-value <hour, featuredCategory> dictionary |
Id connectivity scenario | 7 |
---|---|
Event description | The user edits the details of a product they have created (e.g., updates the name, description, or price) while offline. |
System response | The app saves the edited product information locally as a pending update. When connectivity is restored, the pending changes are automatically synchronized with Firestore, updating the product document. |
Possible Antipatterns | 2, 5, 6 |
Caching + Retrieving strategy | Strategy 4 and 3 from the team's list |
Storage Type | 3.a – Structured local database (SQLite for Flutter) |
Stored data type | Edited product object with fields and a pendingUpdate flag (<PendingEditedProduct> ) |
Id connectivity scenario | 8 |
---|---|
Event description | The user tries to log in while offline. |
System response | The app retrieves the securely stored authentication credentials (token/session) using Flutter Secure Storage. If a valid token exists and is not expired, the user can access offline features; otherwise, login is denied until the connection is restored. |
Possible Antipatterns | 1, 6 |
Caching + Retrieving strategy | Strategy 1 and 5 from the team's list |
Storage Type | 3.c – Flutter Secure Storage |
Stored data type | Secure token/session credentials (accessToken , refreshToken , expirationTime) |
Id connectivity scenario | 9 |
---|---|
Event description | The user explores and applies filters on the marketplace (e.g., by category, price, or recency) while offline. |
System response | The app uses the last successfully fetched products and applies the user's selected filters locally over the cached dataset, ensuring the user can still "search" and "filter" products without needing an active connection. |
Possible Antipatterns | 1, 4, 5 |
Caching + Retrieving strategy | Strategy 1 and 5 from the team's list |
Storage Type | 3.b – Local NoSQL (Hive) |
Stored data type | Cached list of products (cachedProducts ) and the selected filter criteria (cachedFilters ) |
Id connectivity scenario | 10 |
---|---|
Event description | The user sends a chat message to another user, but the device loses internet connection immediately after pressing "Send". |
System response | The message is stored locally in a pending queue. When internet connectivity is restored, the app automatically attempts to send all queued messages to Firestore, ensuring the conversation flow remains intact without user intervention. |
Possible Antipatterns | 1, 3, 5 |
Caching + Retrieving strategy | Strategy 5 (Cache then network) and 4 (Cached on network response) |
Storage Type | 3.b – Local NoSQL (Hive) |
Stored data type | List of pending chat messages (pendingMessages ) with fields like: senderId, receiverId, timestamp, and content |
Considering the crucial role of local storage in managing data on the device, it ensures that, in the event of a loss of connectivity, the information necessary for the operation of the app is available and does not affect the user experience. This ensures the continuity of the functionality of the applications even under offline conditions. Storage strategies were also selected that were aligned with the types of data to be saved, which was crucial in choosing the most appropriate structure. This choice not only facilitated CRUD processes, but also allowed the stored information to be easily accessible and used within the application, maximizing efficiency and performance. The following are the local storage strategies implemented:
What is saved? | Why is it saved? | Where is it saved? |
---|---|---|
User credentials | Saved to ensure the user remains authenticated and to manage their session. It helps provide a seamless experience without the need to log in repeatedly, improving security and usability | local: Flutter secure storage, Kotlin EncryptedSharedPreferences remote: Firebase auth |
Favorite products | Saved to allow users to keep track of products they like, making it easy to access them in future sessions. This provides a more personalized experience, allowing users to revisit their favorite items without reselecting them | local: Flutter Hive, Kotlin Room (SQLite). remote: Firestore |
User data | It is used to store important personal information, such as name, major, and semester, which helps the user to change this information and have it readily available. | local: Flutter SQL BD, Kotlin Room (SQLite). remote: Firestore |
Products | It is stored to retain product details, ensure they are displayed correctly, and allow interaction with them. | local: Flutter SQL BD, Kotlin Room (SQLite). remote: Firestore |
Draft products to be published | Saved to let users work on their products before publishing them, enabling them to save progress and continue later. It also allows for reviewing or editing drafts before finalizing the product. | local: Flutter SQL BD, Kotlin Room (SQLite). remote: Firestore (after confirmantion) |
featured categories per hour | Saved to dynamically show the most relevant categories based on hourly data or user interaction, helping provide up-to-date, personalized content. | local: Flutter Hive, Kotlin Room. remote: Firestore |
In the Flutter implementation, SQLite (through sqflite) and Hive are used as the main local storage solutions. For structured data like user information, products, and favorites, SQLite provides efficient query management and storage, allowing the app to handle these data quickly and reliably. On the other hand, Hive is used for lighter, more easily accessible data like favorite products or featured categories, optimizing read and write speed. For sensitive data, such as user credentials, Flutter Secure Storage is used to ensure that this information is kept secure at all times.
In the Kotlin implementation, Room is used as the main persistence solution for structured data such as products, user information, and favorites. This library abstracts the complexity of SQLite and provides compile-time verification of SQL queries. Additionally, EncryptedSharedPreferences is used to securely store sensitive user data such as credentials. The local Room database enables offline access to product data, efficient querying, and synchronization with Firestore when connectivity is available, ensuring a smooth and reliable user experience.
In Flutter, concurrency refers to the ability to perform multiple operations asynchronously without blocking the main UI thread. Although Flutter is single-threaded by default, it supports asynchronous programming using tools such as:
- Future: Handles single asynchronous operations.
- Stream: Handles continuous sequences of asynchronous events.
These strategies ensure that time-consuming tasks like network calls, database operations or file access do not freeze or slow down the user interface, resulting in a smooth and responsive app experience.
The Future strategy with async/await allows you to execute asynchronous operations sequentially and in a non-blocking manner. It was implemented for tasks such as web service calls, database access, read/write operations, etc.
Features implemented with Multi-threading/concurrency Strategy
- User registration
In the user registration feature, async/await
is used to handle the creation of a new user in Firebase Authentication. This ensures that the app waits for a response from Firebase before continuing execution.
File: auth_repository_impl.dart
Future<String?> signUpWithEmailAndPassword(
String email,
String password,
String name,
String career,
String semester,
) async {
final user = await _authDataSource.signUpWithEmail(email, password);
if (user == null) {
return 'Registration failed.';
}
- Add product
When publishing a product, several asynchronous operations are performed, such as uploading images and saving data to Firestore. Using async/await
allows these operations to be performed sequentially and in a manageable manner.
File: add_product_viewmodel.dart
Future<void> addProduct({
required List<XFile?> images,
required String name,
required String description,
required String category,
required double price,
}) async {
isLoading = true;
errorMessage = null;
notifyListeners();
try {
final product = Product(
id: '',
name: name,
description: description,
category: category,
price: price,
imageUrls: [],
sellerName: '',
favoritedBy: [],
userId: '',
);
await _productRepository.addProduct(images: images, product: product);
} catch (e) {
errorMessage = e.toString();
}
isLoading = false;
notifyListeners();
}
Benefits
- Readability: The code resembles sequential execution, making it easier to understand and maintain.
- Error handling: Allows the use of
try/catch
blocks to effectively capture and handle exceptions. - Efficiency: Avoids blocking the main thread, keeping the application responsive during operations that may take time.
It represents a sequence of asynchronous events received over time. That is, data flows continuously and is listened to as it arrives, such as changes in network connectivity, without blocking application execution.
The ConnectivityService
class uses a Stream<bool>
called isOnline$
that emits the application's connectivity status.
File: connectivity_service.dart
Stream<bool> get isOnline$ async* {
final result = await _connectivity.checkConnectivity();
yield result != ConnectivityResult.none;
yield* _connectivity.onConnectivityChanged.map(
(result) => result != ConnectivityResult.none,
).distinct();
}
Code explanation
Concept | Description |
---|---|
async* and yield
|
Emit the current connectivity state initially |
await |
Asynchronously checks current connectivity status |
yield* with onConnectivityChanged
|
Listens for future connectivity changes and emits updates |
.map(...) | Maps ConnectivityResult to a boolean (true if connected) |
.distinct() | Prevents emitting duplicate values if connectivity state doesn't change |
This strategy is especially useful for allowing the application to dynamically adapt to network availability, such as displaying messages or disabling certain features when there is no Internet access.
Benefits
- Reactivity: Allows the application to react in real time to changes in network connectivity.
- Efficiency: Avoids unnecessary operations by issuing only when there is a change in the connectivity state.
- Simplicity: Using
Stream
andasync/await
provides a clear and concise way to handle asynchronous operations and data flows.
Isolates in Flutter allow performing expensive computations or I/O operations in a separate thread, preventing the main UI thread from being blocked.
In the image caching system, the app needs to delete outdated images from the local cache when a product is updated. If there are many images to remove, doing so on the main thread can cause UI jank.
To avoid this, we apply the Isolate strategy using the compute()
function, which offloads this task to a secondary isolate (thread).
File: edit_product_viewmodel.dart
Future<void> clearEditedImagesFromCache(List<String> oldUrls, List<String> updatedUrls) async {
await compute(_clearImagesWorker, {
'oldUrls': oldUrls,
'updatedUrls': updatedUrls,
});
}
void _clearImagesWorker(Map<String, dynamic> args) async {
final oldUrls = List<String>.from(args['oldUrls']);
final updatedUrls = List<String>.from(args['updatedUrls']);
final manager = CustomCacheManager.instance;
for (final url in oldUrls) {
if (!updatedUrls.contains(url)) {
await manager.removeFile(url);
}
}
}
Concept | Description |
---|---|
compute() |
Flutter function that runs the given callback in a background isolate |
clearEditedImagesFromCache() |
Triggers image cleanup without blocking the UI |
_clearImagesWorker() |
Background function that deletes outdated image URLs |
Benefits
- Performance: Keeps UI responsive by running heavy logic off the main thread.
- Efficiency: Allows concurrent cleanup of cached data.
- Scalability: Suitable for larger apps with heavy I/O or processing.
In our kotlin app, coroutines are used as the primary multi-threading strategy to handle asynchronous operations such as network requests and database interactions efficiently. By leveraging suspend functions and coroutine scopes like viewModelScope, the app ensures that long-running task, such as fetching products from Firestore, are exectuted off the main thread, preventing UI freezes and improving responsiveness. The use of withContext(Dispatchers.IO) explicitly delegates these operations to the I/O thread pool, which is optimized for blocking tasks like reading from or writing to a database.
This coroutine-based approach simplifies concurrency management, ensures lifecycle awareness, and maintains a smooth user experience. The ProductRepository class makes extensive use of suspend functions and await() calls from kotlinx-coroutines-play-services to interact with Firebase Firestore and Firebase Auth. These operations are executed off the main thread, leveraging Dispatchers.IO under the hood.
This multi-threading strategy was used for the next features:
This strategy uses Kotlin coroutines within a HomeScreenViewModel to asynchronously retrieve product data and perform search filtering. All network and data fetching operations are launched using viewModelScope to ensure proper lifecycle handling. Dispachers.IO is implicitly used in suspend functions from the ProductRepository, keeping the main thread free for UI interactions. A debounce strategy is applied to the search query using StateFlow, reducing unnecessary calls while typing.
HomeScreenViewModel
DataLayerFacade
ProductRepository
Following the same logic explained for "Product Retrieval and Searching Filtering Using Coroutines" the list of product changes based on the filter aplied by the user. So the HomeScreenViewModel uses coroutines to asynchronously retrieve the product data. Dispachers.IO is implicitly used in suspend functions from the ProductRepository, keeping the main thread free for UI interactions.
Implemented by: Sara Benavides Mora
HomeScreenViewModel
ProductRepository
This strategy is implemented in FavoritesScreenModel using coroutines and StateFlow. It collects search input changes and filters favorited products accordingly. The debounce(300) operator is applied to delay execution while the user is typing, preventing excessive backend calls. This approach ensures a smooth user experience and optimal resource usage.
Implemented by: Sara Benavides Mora
FavoritesScreenViewModel
It is a strategy that loads images from the Internet and automatically stores them in the device's memory for reuse without having to download them again.
To enhance performance and reduce data usage, SeneMarket implements a robust image caching strategy using the cached_network_image
package. This approach ensures faster load times and an improved user experience by storing images locally after their first download.
Libraries used
-
cached_network_image
: Loads and displays images from the network with automatic caching. -
flutter_cache_manager
: Provides advanced file caching management, used internally bycached_network_image
and extended for custom logic.
Modified and added files for Caching Strategy
File | Type | Description |
---|---|---|
custom_cache_manager.dart |
Added | Defines the custom cache manager with a 30 day stale period |
product_card.dart |
Edited | Uses CachedNetworkImage with custom cache manager for product thumbnails |
my_product_card.dart |
Edited | Applies caching to personal product cards |
product_image_carousel.dart |
Edited | Displays carousel images using CachedNetworkImage
|
edit_product_viewmodel.dart |
Edited | Handles manual cache removal when images are updated |
edit_product_page.dart |
Edited | Applies CachedNetworkImage to show editable product image |
full_screen_image_page.dart |
Edited | Implements CachedNetworkImage for full screen viewing of cached images |
These changes implement a consistent and efficient caching strategy using
cached_network_image
andflutter_cache_manager
.
Custom Cache Manager Configuration
Create a custom cache manager with the following configuration to control caching behavior more precisely:
Configuration | Value | Description |
---|---|---|
Cache key | customProductCache | Unique identifier for the image cache |
Stale Period | 30 days | Duration after which cached items are considered stale and will be re-fetched from the source |
Maximum number of cache objects | 100 | Maximum number of items to retain in the cache to save disk space |
This configuration ensures that cached images older than 30 days or exceeding 100 items are automatically removed.
Handling image updates
When product images are updated or deleted (for example, during product edits), outdated images must be cleared from the cache to avoid displaying stale data.
Implement a method clearEditedImagesFromCache
to remove specific images from the cache when they are no longer valid. Call this method after updating images to ensure that the cache reflects the latest content.
Benefits
- Faster load times: Cached images are loaded locally.
- Lower data usage: Avoids redundant downloads.
- Better UX: Users experience smooth scrolling and reduced loading spinners.
In our Kotlin application, an LRU (Least Recently Used) caching strategy is employed for loading product images efficiently. This approach ensures that the most frequently accessed images are kept in memory, while older, less-used images are evicted when the cache reaches its capacity. By leveraging LRU caching, implemented by the library of Coil, the app minimizes redundant network requests and accelerates image loading times, especially when users scroll throug product lists or revisit previosly viewed items. This improves overall performance and provides a smoother, more responsive user experience, particularly in resource-constrained environments.
Based on our architecture, which separates responsibilities across the View, ViewModel, DataLayerFacade, and Repository layers, the implementation of an LRU caching strategy is applied specifically at the View layer, where images are loaded for product display. The image caching is handled using the Coil library, which internally leverages an LRU cache mechanism to store and reuse image bitmaps efficiently. This ensures that frequently accessed product images, such as those shown on the Home screen, Favorites, or as search results, are quickly loaded from memory instead of being re-downloaded from the network, resulting in faster performance and reduced data usage. In the UI, product images are loaded using Coil’s AsyncImage composable. The next example applies for every section of the app where the product image is shown such as HomeScreen, ProductDetailScreen and FavoritesScreen.