Repository navigation
Sprint 3
In this sprint we made CampusSwap usable on unreliable campus Wi-Fi. Every core flow now has an answer for losing the connection, from browsing and publishing to chatting, confirming the exchange and rating the other student. To support it we added local storage, caching, background synchronization and concurrency strategies. We also completed features started in Sprint 2: the real backend chat, account registration, form validation and server-side Smart Matching.
Eventual connectivity means the app keeps working when the connection drops or comes and goes, which is common on campus Wi-Fi (elevators, basements, crowded classrooms). For each scenario we describe the event, how the app responds, the anti-patterns we avoid, and where data is stored. Storage types follow the classification from the lectures (Local > Private > DB / Preferences / Files / Cache).
| Event description | The user opens Home, or is already on it, and the phone has no internet or the server can't be reached. |
| System response | Home shows the last listings downloaded from the server instead of an empty screen. An amber banner says "You're offline. Showing listings saved 5 min ago." When the connection returns, the banner disappears and the feed refreshes on its own, with no user action. |
| Possible anti-patterns | Lost content (empty feed), Blocked application, Non-informative message |
| Caching and retrieving strategy | Network falling back to cache |
| Storage type | Local > Private > DB (Room, table cached_materials) |
| Stored data type | Rows with each listing's fields, its position in the feed and the time it was cached |
| Author | Daniel Diab |
| Event description | The seller finishes the 4-step sell form and taps Publish while offline, or the connection drops while publishing. |
| System response | The listing is saved on the phone, and the confirmation screen says "Saved on your phone. We'll publish it automatically as soon as you reconnect, even if you close the app." The listing appears right away in the feed and in the seller's profile, labeled "Waiting to publish". When there is a connection, a background job sends it (POST /materials) and the real listing replaces the local copy. If the server rejects it, the listing shows the server's reason and a Discard option. |
| Possible anti-patterns | Lost content (lost form data), Stuck progress notification, Blocked application |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > DB (Room, table pending_listings) |
| Stored data type | One row per queued listing: title, description, course, price, condition, category, owner, attempts, failure reason |
| Author | Daniel Diab |
| Event description | While writing a listing, the user loses signal, leaves the screen or the OS kills the app. |
| System response | The form saves itself while the user types, about 0.4 s after they stop. When the user opens Sell again, the draft is restored with the message "We restored the draft you left unfinished." Closing the form asks whether to keep the draft or discard it. |
| Possible anti-patterns | Lost content (lost text) |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > Preferences (Jetpack DataStore, sell_draft) |
| Stored data type | The draft serialized as JSON (Gson) under one key |
| Author | Daniel Diab |
| Event description | The buyer or seller writes a message, or sends a photo, while offline. |
| System response | The message appears in the thread right away, with a cloud icon ("Waiting for connection"), and a banner explains it will be sent on reconnect. Messages wait in the outbox and are sent in order once the network is back (open room → POST /messages). The icon changes to a tick when the server has the message. Queued messages survive an app restart. |
| Possible anti-patterns | Lost content (lost message), Stuck progress notification, Non-informative message |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > DB (Room, table pending_messages) |
| Stored data type | One row per message: local id, owner, listing, content (text or uploaded photo URL), attempts, failure reason |
| Author | Daniel Diab (outbox and sync), Carla González Mina (real chat screen and photos) |
| Event description | The user opens a chat or the conversations inbox while offline, even right after restarting the app. |
| System response | The chat shows the conversation as it was the last time it loaded, with the counterpart's name, and the banner says "You're offline. Showing this conversation as it was 10 min ago. New messages are sent when you reconnect." The inbox does the same ("Showing your conversations as they were 10 min ago"). Messages still waiting in the outbox appear after the saved ones. The saved copy never marks messages as read and never shows an old unread count. If nothing was ever saved for that chat, the screen shows an offline state instead of an infinite spinner. Everything refreshes on its own when the connection returns. |
| Possible anti-patterns | Lost content (empty chat), Stuck progress notification, Blocked application, Non-informative message |
| Caching and retrieving strategy | Network falling back to cache |
| Storage type | Local > Private > DB (Room, tables cached_responses and pending_messages) |
| Stored data type | The last server response of each thread, of the inbox and of the counterpart's name, as JSON, keyed by user and listing, with the time it was saved |
| Author | Carla González Mina (offline state), Daniel Diab (saved threads and inbox) |
| Event description | The user opens the meeting point screen and the meeting points or the shared free times can't be downloaded. |
| System response | The app uses the meeting points loaded in this session or, after a restart, the copy saved on the phone, and only then the pre-mapped safe campus points bundled with the app. The screen says "Offline: showing the campus zones saved on this phone." The shared free times also come from the last saved copy, without the ones that already passed, so the user can still pick a place and a time and propose the meeting (see EC11). |
| Possible anti-patterns | Lost content (blank map, no times), Blocked application |
| Caching and retrieving strategy | Network falling back to cache, then to bundled data |
| Storage type | Local > Private > DB (Room, cached_responses) + bundled app data |
| Stored data type | List of meeting points (shared by every account) and the free times of each listing (per user) as JSON |
| Author | Carla González Mina (bundled points and offline state), Daniel Diab (saved copy and free times) |
| Event description | The app records analytics events (screen views, listings opened, messages, smart matches) while there is no connection, or the app crashes. |
| System response | Events are written to disk first and sent to the backend in batches of 20, or every 15 s. A batch that fails stays queued for the next attempt, so no business-question data is lost. |
| Possible anti-patterns | Lost content (lost data) |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > Files (pending.jsonl) |
| Stored data type | One JSON line per event |
| Author | Daniel Diab (disk queue), Carla González Mina (HTTP sink to the backend) |
| Event description | The user tries to log in or register while offline. |
| System response | The button shows progress only while the request is in flight. Then a clear message appears ("Can't reach the server. Check your connection and try again.") and the typed data stays in the form. |
| Possible anti-patterns | Stuck progress notification, Non-informative message, Lost content (lost text) |
| Caching and retrieving strategy | N/A |
| Storage type | N/A |
| Stored data type | N/A |
| Author | Daniel Diab |
| Event description | The buyer is at the meeting point and taps "I'm here — confirm with GPS" (or confirms by hand), but the phone has no signal, which is common inside buildings. |
| System response | The GPS check still runs on the phone. The confirmation, with the measured coordinates, is saved and the screen says "You're at the spot, but offline. The confirmation is saved on your phone and sent as soon as you reconnect." The rating step is unlocked right away. When there is a connection, the background job places and completes the order on the server (POST /exchanges/{id}/complete) and the screen changes to Confirmed on its own, even if the user left it. If the server refuses it, the screen shows the server's reason once and lets the user try again. |
| Possible anti-patterns | Lost content (lost confirmation), Blocked application, Stuck progress notification |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > DB (Room, table pending_actions) |
| Stored data type | One row per action: kind CHECK_IN, listing, owner, JSON payload (buyer, seller, price, meeting point, latitude, longitude), attempts, failure reason |
| Author | Daniel Diab |
| Event description | After the exchange, the user writes the rating (stars, tags, review) and taps Send rating while offline. |
| System response | The rating is saved on the phone and the screen says "You're offline. Your rating is saved on your phone and sent as soon as you reconnect." It is sent after the check-in of the same exchange, never before, because the server only accepts ratings for completed exchanges. If the app is restarted, the written rating is shown again from the saved copy. When it is delivered, the message changes to "Saved — it now counts toward their rating." |
| Possible anti-patterns | Lost content (lost review text), Non-informative message |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > DB (Room, table pending_actions) |
| Stored data type | One row with kind RATING and a JSON payload (rated user, stars, tags, review, confirmed condition) |
| Author | Daniel Diab |
| Event description | The user picks a meeting point and a free time and taps Propose while offline. |
| System response | The proposal is saved and the user goes back to the chat, where it appears as their own message ("Meeting proposal: Library, Mon 5 Oct 12:00 – 13:00") with the cloud icon. When there is a connection it is sent (POST /meeting-proposals) and replaced by the real proposal card. Before sending, the app checks the time hasn't already passed. If it has, the message says "Meeting proposal not sent: That time already passed before you got back online. Propose a new one."
|
| Possible anti-patterns | Lost content, Non-informative message, outdated data sent as if it were current |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > DB (Room, table pending_actions) |
| Stored data type | One row with kind PROPOSAL and a JSON payload (meeting point, time slot with ISO start and end) |
| Author | Daniel Diab |
| Event description | The user scrolls listings or opens a chat with photos while offline, including after closing the app. |
| System response | Photos that were seen before load from the phone, with no spinner and no data use. A listing photo that was never downloaded shows the category placeholder instead of an empty box. |
| Possible anti-patterns | Lost content (empty images), Stuck progress notification |
| Caching and retrieving strategy | Cache, then network |
| Storage type | Local > Private > Cache (memory LruCache + files in cacheDir/images) |
| Stored data type | Decoded bitmaps in memory and the original image bytes on disk, one file per URL |
| Author | Daniel Diab |
| Event description | The user opens the wishlist, the notifications or the alerts screen while offline, even after restarting the app. |
| System response | Each screen shows the copy saved the last time it loaded. Notifications say "You're offline. Showing the last notifications we loaded." Alerts say "You're offline. These are the alerts saved on this phone; changes need a connection." Changes to alerts show up immediately. If the server can't save one, it is undone and the screen says "We couldn't save that change to your alerts. Check your connection and try again." |
| Possible anti-patterns | Lost content, Non-informative message, Inconsistent state (a change that looks saved but isn't) |
| Caching and retrieving strategy | Network falling back to cache |
| Storage type | Local > Private > DB (Room, cached_responses) |
| Stored data type | Last server response of the wishlist, notifications and alerts as JSON, per user |
| Author | Daniel Diab |
In the Flutter app an offline situation reaches the code as a NetworkException thrown by ApiClient (no answer, DNS failure or the 20 s timeout). Until Sprint 2 every repository turned it into the message "Can't reach the server" and the user lost whatever was in progress. In Sprint 3 each core flow has an answer for it. Storage types follow the classification from the lectures (Local > Private > DB / Preferences / Files / Cache).
| Event description | The user opens Home, or is already on it, and the phone has no internet or the server can't be reached. |
| System response | Home shows the last listings downloaded instead of an empty screen. An amber banner says "You're offline. Showing listings saved 5 min ago." When the connection returns, the banner disappears and the feed refreshes on its own. If nothing was ever saved (first launch offline), the screen shows "Connect once to load listings" with a Retry button, not an endless spinner. |
| Possible anti-patterns | Lost content (empty feed), Blocked application, Non-informative message. |
| Caching and retrieving strategy | Network falling back to cache. |
| Storage type | Local > Private > DB (sqflite, table cached_responses) |
| Stored data type | One JSON row per user and screen (materials:) with the time it was saved. |
| Author | Sabrina Noguera |
| Event description | The user types in the search box or applies filters and sorting while offline. |
| System response | The search runs on the listings saved on the phone, so the filters and the SortStrategy keep working. A bar above the results says "Waiting for connection. Searching the listings saved on this phone." The empty state says whether it is empty because nothing matches or because nothing was saved. When the connection returns, the listings refresh and the results update by themselves. |
| Possible anti-patterns | Non-informative message ("No items found" with no reason), Blocked application, Lost content. |
| Caching and retrieving strategy | Network falling back to cache. |
| Storage type | Local > Private > DB (sqflite, cached_responses) |
| Stored data type | One JSON row per user and screen (materials:) with the time it was saved. (The listings JSON of EC1) |
| Author | !!!!! |
| Event description | The seller finishes the new-listing form and taps Publish while offline, or the connection drops while the photos are uploading. |
| System response | The photos are copied to the app's own folder and the listing is saved on the phone. The confirmation says "Saved on your phone. We'll publish it as soon as you reconnect." The listing shows at the top of My Listings with the label "Waiting to publish" (it is not in the public feed yet). When there is a connection, the sync service uploads the photos (POST /uploads) and then creates the listing (POST /materials), and the real listing replaces the local one. Photos already uploaded in a failed attempt are not uploaded again. If the server refuses it, the listing shows the server's reason with Retry and Discard. |
| Possible anti-patterns | Lost content form data and photos, Stuck progress notification, Blocked application |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > DB (sqflite, table pending_listings) + Local > Private > Files (pending_photos/) |
| Stored data type | One row per queued listing: client id, owner, title, description, course, price, condition, category, photo paths, already uploaded URLs, attempts, failure reason |
| Author | Angie Gutiérrez |
| Event description | While filling the form, the user loses signal, switches to another app or the OS kills the app. Today the form lives only in TextEditingControllers and widget state, so everything is lost. |
| System response | The form saves itself about 0.4 s after the user stops typing, including the chosen photos. When the user opens the form again, the draft is restored with the message "We restored the draft you left unfinished." Closing the form asks whether to keep or discard the draft. The draft is deleted once the listing is queued or published. |
| Possible anti-patterns | Lost content such as text and photos |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > Preferences (shared_preferences, key sell_draft) + Files for the photos |
| Stored data type | The draft as one JSON string (title, description, course, price, condition, category, photo paths) |
| Author | Angie Gutiérrez |
| Event description | A student fills the registration form, or logs in, and the phone has no connection. |
| System response | A "Waiting for connection" bar is shown while offline. The button shows progress only while the request is in flight; if it fails, the message says "Can't reach the server. Check your connection and try again." and everything typed stays in the form. The registration data is also saved while typing, so if the app is closed the student returns to the form already filled. Passwords are never written to disk: the student only has to type them again. |
| Possible anti-patterns | Lost content (lost text), Stuck progress notification, Non-informative message, storing credentials in plain text |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > Preferences (shared_preferences, key register_draft) |
| Stored data type | JSON with the non-sensitive fields (name, major, institutional email). Password and confirmation are excluded |
| Author | Sabrina Noguera. |
| Event description | The user opens the app with a valid token in secure storage, but there is no connection. Now, restoreSession() catches the NetworkException and returns null, which sends the user to the login screen. |
| System response | The app opens with the profile saved at the last sign-in instead of the login screen, and keeps the token so requests work as soon as the connection is back. When the connection returns or the app comes back to the foreground, it validates the token (GET /auth/me), replaces the profile with the fresh one and loads again what could not load offline. If the server answers 401, the session ends and the login screen asks for the password again; queued items stay saved under their owner. Fingerprint Quick Unlock also works offline for the enrolled account. Without a saved profile (never signed in on this phone), the login screen is shown. |
| Possible anti-patterns | Blocked application (cannot get in without Wi-Fi), Lost content |
| Caching and retrieving strategy | Network falling back to cache |
| Storage type | Local > Private > Preferences (flutter_secure_storage, JWT, already used) + Local > Private > DB (cached_responses, key signed_in_profile) |
| Stored data type | The JWT, and the last user profile as JSON |
| Author | Andrés Botero |
| Event description | The buyer or seller writes a message while offline, or the connection drops while sending. |
| System response | Like WhatsApp: the message appears in the thread right away with a clock icon ("Waiting for connection") and a banner explains it will be sent when the connection returns. Messages wait in the outbox and are sent in order once the network is back (open room, then POST /messages). The icon changes to a tick when the server has it, and later to the existing read receipt. Queued messages survive an app restart. If the server refuses one, it shows "Not sent" with Retry and Delete, and the messages after it in that conversation wait, so they never arrive out of order. |
| Possible anti-patterns | Lost content (lost message), Stuck progress notification, Non-informative message |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > DB (sqflite, table pending_messages) |
| Stored data type | One row per message: client id (UUID), owner, chat room, content, creation time, attempts, status, failure reason |
| Author | Sabrina Noguera. |
| Event description | The user opens a chat or the conversations list while offline, even right after restarting the app. |
| System response | The chat shows the conversation as it was the last time it loaded, and the banner says "You're offline. Showing this conversation as it was 10 min ago. New messages are sent when you reconnect." The inbox does the same. Messages still in the outbox appear after the saved ones. The saved copy never marks messages as read and never shows an old unread count. The 4 s and 12 s polling timers are paused while offline (this also saves battery) and restart when the connection returns. If nothing was ever saved for that chat, the screen shows an offline state instead of an infinite spinner. |
| Possible anti-patterns | Lost content (empty chat), Stuck progress notification, Blocked application, Non-informative message |
| Caching and retrieving strategy | Network falling back to cache |
| Storage type | Local > Private > DB (sqflite, cached_responses and pending_messages) |
| Stored data type | The last server response of each thread and of the inbox as JSON, keyed by user and chat room, with the time it was saved |
| Author | Sabrina Noguera. |
| Event description | The user opens the meeting point screen and the zones or the shared free times can't be downloaded. |
| System response | The app uses the zones loaded in this session or, after a restart, the saved copy, and only then the safe campus zones bundled with the app. The screen says "Offline: showing the campus zones saved on this phone." The GPS distance and walking-time ranking keep working, because the sensor and the calculation are on the phone. The OpenStreetMap background may not load offline, so the markers are drawn over a plain background and the zone list below stays usable. Free times come from the last saved copy without the slots that already passed, so the user can still choose a place and a time and propose the meeting (EC12). |
| Possible anti-patterns | Lost content (blank map, no times), Blocked application |
| Caching and retrieving strategy | Network falling back to cache, then to bundled data |
| Storage type | Local > Private > DB (sqflite, cached_responses) + bundled app asset (assets/campus_zones.json) |
| Stored data type | The zone list (shared by every account) and the free times of each chat (per user) as JSON |
| Author | Angie Gutiérrez |
| Event description | The buyer is at the meeting point, checks the item, picks the condition received, rates the seller and taps Complete exchange, but the phone has no signal, which is common inside buildings. |
| System response | The confirmation (with the received condition) and the rating are saved on the phone, in that order, and the screen says "You're offline. It's saved on your phone and will be sent when you're back online." Reopening the exchange shows that it is saved and waiting. When there is a connection, the sync service calls POST /exchanges/{id}/complete and then sends the rating, and the orders are read again so the exchange shows as completed by itself, even if the user left the screen. If the server refuses the confirmation (for example because the exchange was cancelled), the rating is held back and the screen shows the reason with a Discard button. |
| Possible anti-patterns | Lost content (lost confirmation), Blocked application, Stuck progress notification |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > DB (sqflite, table pending_actions) |
| Stored data type | One row per step: kind completeExchange, exchange id as its group, owner, JSON payload (received condition), attempts and failure reason |
| Author | Andrés Botero |
| Event description | After the exchange the user is writing the rating (stars, tags, review) and the connection drops or the app closes, or taps Send rating while offline. |
| System response | While writing, the checklist, received condition, stars, tags and review are saved as a draft 0.4 s after the last change (or when leaving the screen), and restored when the screen reopens ("Your unfinished rating was restored."). If the user taps Send offline, the rating is saved on the phone and the screen says "You're offline. It's saved on your phone and will be sent when you're back online." It is sent after the confirmation of the same exchange, never before, because the server only accepts ratings for completed exchanges. The draft is deleted once the rating is sent or queued. |
| Possible anti-patterns | Lost content (lost review text), Non-informative message |
| Caching and retrieving strategy | N/A |
| Storage type | Draft: Local > Private > Preferences (shared_preferences, rating_draft:<userId>:<exchangeId>). Queued rating: Local > Private > DB (pending_actions) |
| Stored data type | Draft JSON (checked items, received condition, stars, tags, review). One row with kind rate and a JSON payload (rated user, stars, tags, review) |
| Author | Andrés Botero |
| Event description | The user has the meeting planner open (zones and free times already loaded), picks a meeting point and a free time, and taps Propose while offline. |
| System response | The proposal is saved on the phone and the user goes back to the chat with the message "You're offline. The proposal is saved and will be sent to when you're back online." When there is a connection, the sync service sends it and the proposal card appears in the chat. Before sending, it checks that the time has not passed; if it has, the proposal is refused on the phone with "That time already passed. Propose another one." instead of being sent. |
| Possible anti-patterns | Lost content, Non-informative message, outdated data sent as if it were current |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > DB (sqflite, pending_actions) |
| Stored data type | One row with kind proposeMeeting, the chat as its group and a JSON payload (meeting point, start and end time in ISO format) |
| Author | Andrés Botero |
| Event description | The phone has no Wi-Fi but has mobile data, and the user has not allowed the app to sync over mobile data. Many students have limited plans, and photos are the heaviest part of the app. |
| System response | A bar says "Waiting for Wi-Fi. Turn on 'Use mobile data' in Profile to send now." Browsing and reading still work, because they are small requests. The outbox (messages, listings with photos, check-ins, ratings) waits for Wi-Fi unless the user turns the option on, or taps "Send now over mobile data" for a single sync. The option is off by default and is remembered. |
| Possible anti-patterns | Non-informative message, Lost content (queued items that silently never leave), unexpected data use |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > Preferences (shared_preferences, key allow_mobile_data) |
| Stored data type | One boolean |
| Author | Sabrina Noguera. |
| Event description | The user scrolls listings or opens a chat with photos while offline, including after closing the app. |
| System response | Photos seen before load from the phone, with no spinner and no data use. A photo that was never downloaded shows the category placeholder instead of an empty box or an error. |
| Possible anti-patterns | Lost content (empty images), Stuck progress notification |
| Caching and retrieving strategy | Cache, then network |
| Storage type | Local > Private > Cache (Flutter ImageCache in memory + cached_network_image files in the cache folder) |
| Stored data type | Decoded images in memory and the original image bytes on disk, one file per URL |
| Author | Sabrina Noguera |
| Event description | The user opens the wishlist or the notifications while offline, even after restarting the app. |
| System response | Each screen shows the copy saved the last time it loaded, with a banner "You're offline. Showing the copy saved 2h ago." and a Retry button. The lists load on their own, so they show even when the listings or the orders cannot load. Tapping the heart already updates instantly; if the server cannot save the change, it is undone and the screen says "Can't reach the server. Check your connection and try again." Opening a notification offline is also undone, so the unread dot never lies. After a change, the saved copy is refreshed so a restart does not bring back what was undone. |
| Possible anti-patterns | Lost content, Non-informative message, Inconsistent state (a change that looks saved but isn't) |
| Caching and retrieving strategy | Network falling back to cache |
| Storage type | Local > Private > DB (sqflite, cached_responses) |
| Stored data type | The last server response of the wishlist and the notifications as JSON, per user |
| Author | Andrés Botero |
| Event description | The app records analytics events (screens, listings opened, searches, messages, meeting confirmed) while there is no connection or the app crashes. |
| System response | Events are written to disk first and sent in batches of 10, or every 30 s. A batch that fails stays queued for the next attempt, so no data is lost. Each event keeps the time it really happened (not the time it was sent) and a client UUID, so a retried batch never creates duplicates. |
| Possible anti-patterns | Lost data and duplicated data |
| Caching and retrieving strategy | N/A |
| Storage type | Local > Private > Files (pending.jsonl) |
| Stored data type | One JSON line per event |
| Author | Angie Gutiérrez |
-
Network monitoring.
NetworkConnectivityObserverlistens to Android'sConnectivityManagerdefault network and exposesisOnlineas aStateFlow. The UI collects it to show offline banners in Home, Sell and Chat, and the app uses it to react when the connection comes back. - Online-first with cache fallback (Home, chats, inbox, meeting points, free times, wishlist, notifications, alerts). The app always asks the server first and saves every successful answer. If the request fails, it shows the last saved copy and says how old it is. Copies are saved per user, so another account on the same phone never sees them.
- Offline-first writes with an outbox (publishing, chat, check-in, rating, meeting proposal). User actions are first written to Room and shown in the UI immediately. Sending them is a separate step, so the user never waits on the network or loses what they did.
-
Background synchronization with WorkManager.
OutboxWorkerruns only with aCONNECTEDnetwork constraint. It retries with exponential backoff and survives app restarts and reboots, so queued items are sent even if the app is closed when the signal returns. The work is unique (APPEND_OR_REPLACE), so a sync that is already running is never duplicated. - Clear ending for every item. Each queued item ends as sent (removed), retry later (no network, 5xx, expired session) or refused (4xx, shown with the server's reason). Messages to the same conversation stop at the first failure, so they never arrive out of order.
- Ordered delivery. The outbox sends listings, then messages, then exchange actions, oldest first, and stops at the first one that must wait. A rating never reaches the server before the check-in of the same exchange.
- Validate before sending late. A meeting proposal saved offline is checked again before it is sent; if its time already passed, it is refused on the phone with an explanation instead of sending an outdated proposal.
- Screens follow the outbox. Screens observe the outbox tables, so a check-in, a rating or a proposal changes from saved to confirmed by itself when the background job delivers it.
- Optimistic updates with rollback (smart alerts). Creating, pausing or deleting an alert shows up immediately. If the server doesn't confirm it, the change is undone and the user is told why.
-
Server-side Smart Matching. Alerts live on the server, which creates the
SMART_MATCHnotification when a matching listing is published. The match is not lost if the interested student's phone is offline or the app is closed at that moment; it shows up the next time notifications load. - Per-user outbox. Each queued row stores its owner. Only that user's session sends it, so another account logging in on the same phone never posts in someone else's name.
- Refresh on reconnect. When the connection returns, Home, the wishlist, the alerts and the notifications reload and the outbox is flushed immediately.
-
Network monitoring. A
ConnectivityServicewrapsconnectivity_plusand exposes one state (online,offline,waitingForWifi) as aValueNotifier. The interface type alone does not prove that the server is reachable, so aNetworkExceptionfromApiClientalso sets the state tooffline, and the next successful request returns it to 'online'. Screens listen to it to show the banners, and the app uses it to react when the connection returns.
Why: one source of truth, so no screen guesses its own connection state.
- Online-first with cache fallback (Home, search, chats, inbox, meeting points, free times, wishlist, notifications, profile). The app always asks the server first and saves every successful answer. If the request fails, it shows the last saved copy and says how old it is. Every load replaces the saved copy, so sold or removed listings disappear. Copies are saved per user.
Why: the server is the source of truth because listings and chats change often.
- Offline-first writes with an outbox (publishing, chat, check-in, rating, meeting proposal). The action is first written to sqflite and shown in the UI immediately. Sending it is a separate step, so the user never waits on the network and never loses what they did.
Why: these are the actions where losing the data costs the user the most.
-
Sync service.
OutboxSyncServiceflushes the outbox when the connection returns, the app comes back to the foreground (WidgetsBindingObserver), a retry timer with exponential backoff fires while the app is open, or aworkmanagertask runs while the app is closed (Android). Only one flush runs at a time.
Why: the student usually reconnects when the app is not even open.
-
Clear ending for every item. Each queued item ends as sent, retry later (no network, 5xx error, expired session) or refused (4xx error, shown with the server's reason). Messages to the same conversation stop at the first failure, so they never arrive out of order.
-
Ordered delivery with dependencies. The outbox sends listings, then messages, then exchange actions, oldest first. A rating never goes before the check-in of the same exchange.
-
Idempotent retries. Every queued item has a client UUID that is sent with the request. If a request reached the server but the answer was lost, sending it again must not create a second message or listing.
-
Draft auto-save with debounce. The new-listing form, the registration form (without passwords) and the rating are saved about 0.4 s after the user stops typing, and are restored when the screen opens again.
Why: the OS can kill the app at any time and the form state lives only in memory.
-
Validate before sending late. A meeting proposal saved offline is checked again before it is sent. If its time has passed, it is refused on the phone with an explanation.
-
Screens follow the outbox. Screens listen to the outbox, so a check-in, a rating or a proposal changes from saved to confirmed by itself when the sync service delivers it.
-
Optimistic updates with rollback (wishlist, notifications). The change shows immediately. If the server does not confirm it, the change is undone, and the user is told why. This is already how
MarketplaceViewModel.toggleFavoriteworks. -
Per-user outbox. Each queued row stores its owner. Only that user's session sends it, so another account on the same phone never posts in someone else's name, and logging out never deletes another user's pending work.
-
Optional mobile data. The sync service checks the
allow_mobile_dataoption before it flushes. With it off and no Wi-Fi, items wait and the user is told why. -
Offline session start. If the token is stored but the network is down at launch, the app opens with the saved profile instead of the login screen, and validates the token when the connection returns.
-
Refresh on reconnect. When the connection returns, Home, the wishlist, the notifications, and the open chat reload, then the outbox is flushed immediately.
| Where | Strategy | Tool | Why | Author |
|---|---|---|---|---|
| Home listings | Network falling back to cache. Every successful response replaces the cache, so sold or removed listings disappear. | Room (cached_materials) |
Listings change often, so the server is the source of truth. The cache only guarantees the feed is never empty offline and keeps the same order as the server. | Daniel Diab |
| Photos (listings and chat) | Cache, then network, in two levels: memory, then disk, then download. |
LruCache with 1/8 of the heap, sized by each bitmap's bytes, + disk cache of 50 MB in cacheDir/images that removes the least recently used files (ImageCache, ImageDiskCache) |
Scrolling shouldn't download or decode the same photo again, which saves mobile data, battery and memory, and photos seen before still show offline. Photos are reduced to 1080 px before going into memory, and only bytes that decode as an image are saved to disk. | Daniel Diab |
| Chat threads, inbox and counterpart name | Network falling back to the last response for the same user | Room (cached_responses) |
A conversation must be readable offline, but the server is the source of truth, so every load replaces the saved copy. | Daniel Diab |
| Meeting points | Network falling back to the session copy, then to the saved copy, then to bundled data | In-memory copy + Room (cached_responses) + bundled points |
Safe campus spots rarely change, and the meeting flow must always offer a place. | Carla González Mina, Daniel Diab |
| Free times for a meeting | Network falling back to the last response, minus the times already past | Room (cached_responses) |
Lets the user propose a meeting offline without offering a time that is already over. | Daniel Diab |
| Notifications | Network falling back to the session copy, then to the saved copy for the same user | In-memory copy + Room (cached_responses) |
Notifications stay visible after a failed refresh or a restart, and are never shown to another user. | Carla González Mina, Daniel Diab |
| Wishlist and smart alerts | Network falling back to the last response for the same user | Room (cached_responses) |
The user can check what they saved and what they are waiting for, even offline. | Daniel Diab |
| Where | Strategy | Tool | Why | Author |
|---|---|---|---|---|
| Home listings and search | Network falling back to cache. Every successful response replaces the cache, so sold or removed listings disappear. | sqflite (cached_responses) |
Listings change often, so the server is the source of truth. The cache only guarantees the feed and the search are never empty offline. | Sabrina Noguera. |
| Photos (listings and chat) | Cache, then network, in two levels: memory, then disk, then download. | Flutter ImageCache (size set at start-up) + cached_network_image / flutter_cache_manager (limit of 200 files, 30 days) |
Scrolling should not download or decode the same photo again. It saves mobile data, battery and memory, and photos seen before still show offline. | Sabrina Noguera |
| Chat threads, inbox and counterpart name | Network falling back to the last response for the same user. | sqflite (cached_responses) |
A conversation must be readable offline, but the server is the source of truth, so every load replaces the saved copy. | Sabrina Noguera |
| Meeting points | Network falling back to the session copy, then to the saved copy, then to bundled data. | In-memory copy + sqflite + assets/campus_zones.json
|
Safe campus zones rarely change, and the meeting flow must always offer a place. | Sabrina Noguera, Angie Gutiérrez |
| Free times for a meeting | Network falling back to the last response, minus the times already past. | sqflite (cached_responses) |
Lets the user propose a meeting offline without offering a time that is already over. | Angie Gutiérrez |
| Wishlist and notifications | Network falling back to the last response for the same user. | sqflite (cached_responses) |
They stay visible after a failed refresh or a restart and are never shown to another user. | Andrés Botero |
| Signed-in profile | Network falling back to the last saved profile. | sqflite (cached_responses) + secure storage for the token |
Lets the app open offline with a valid session. | Andrés Botero |
Android allows real multi-threading. We use Kotlin coroutines with dispatchers so the main (UI) thread only renders, and any work that may block runs on another thread.
-
Dispatchers.Defaultfor CPU work. Mapping server DTOs to Room entities and to UI models (MaterialRepository), and turning saved responses to and from JSON (ResponseCache), run on Default, so large lists and long threads don't freeze scrolling. -
Dispatchers.IOfor blocking I/O. Image downloads, disk reads and decoding (ImageCache) and session persistence (SessionManagerscope) run on IO. -
One download per image, shared by everyone who asks. When several cards ask for the same photo at once,
ImageCachekeeps the download in progress in aConcurrentHashMap<String, Deferred>and every caller awaits the same job. The job runs in the cache's ownSupervisorJobscope, so a card that scrolls away doesn't cancel the download for the others. -
Thread-safe caches.
LruCacheis safe across threads. The disk cache uses@Synchronizedmethods, and writes go to a temporary file that is then renamed, so a photo is never half-written if the app dies. -
Room's own executor. All DAO calls are
suspendfunctions orFlows, so Room runs them off the main thread and the UI observes the outbox reactively. -
Reactive streams (
Flow/StateFlow). The network state, the outbox tables and the chat state are streams. The ViewModels collect them inviewModelScope, and Compose recomposes only what changed. -
Background worker (WorkManager +
CoroutineWorker). Outbox sync runs outside the UI lifecycle, in its own worker thread, with constraints and retries managed by the OS. It sends items one by one, in order, so actions that depend on each other never race. -
Debouncing with
collectLatest. Draft auto-save waits 400 ms after the user stops typing, and cancels the previous pending write, instead of writing to disk on every key. -
Periodic polling in a coroutine. The open chat refreshes on a timer inside
viewModelScope, and the job is cancelled when the screen closes. - Batching. Analytics events are sent in batches by size or time instead of one request per event.
-
Reacting to memory pressure. When the app goes to the background (
onTrimMemory), half of the image memory cache is released; the disk copy stays. -
Thread-safe callbacks. The
ConnectivityManagercallback runs on a system thread and only writes to aStateFlow, which is safe to read from the main thread.
Dart runs your code in one thread per isolate (the UI isolate), with an event loop. async/await does not block that thread while it waits for the network or the disk, but heavy calculations do. So we use Futures for waiting work and isolate for CPU work, so the UI isolate only renders.
-
async/awaitfor I/O. Network calls (http) and database calls (sqflite runs its queries on a native background thread) never block the UI. -
Isolates for CPU work (
Isolate.run/compute). Decoding and mapping large JSON lists (listings, long chat threads), encoding the saved responses to JSON, and resizing and compressing photos before they are uploaded or queued (max 1080 px). Without this, scrolling and typing would freeze on a mid-range phone. -
Parallel loading with
Future.wait. Listings and wishlist, and exchanges and notifications, already load at the same time (MarketplaceViewModel.load,AccountViewModel.load). Sprint 3 does the same for the photos of one listing (up to 3 uploads at once;Future.waitkeeps their order). -
Single-flight sync. The outbox flush has a lock (
Completer), so the "connection returned", "app resumed" and "timer" triggers can never run two flushes at once and send an item twice. -
Debouncing with
Timer. The draft auto-save waits 400 ms after the user stops typing and cancels the previous pending write; the search box waits 300 ms. -
Periodic polling with
Timer.periodic. The open chat refreshes every 4 s and the inbox every 12 s (already inChatViewModel). -
Streams and listenables. The connection state is a stream (
onConnectivityChanged) that feeds aValueNotifier. The outbox and the states areChangeNotifiers, and the screens rebuild only what changed withListenableBuilder. -
Dropping stale answers.
MarketplaceViewModelandAccountViewModelalready use a_generationcounter, so an answer that arrives after a logout is ignored. The new states reuse it. -
Background task (
workmanager). The sync runs outside the app lifecycle, in its own isolate, with a "network connected" constraint and OS-managed retries. That isolate has to build its own database connection,ApiClientand token. On iOS the system decides when background tasks run, so there the sync is guaranteed only when the app is open or comes to the foreground. - Batching. Analytics events are sent in batches by size or time instead of one request per event.
-
Reacting to memory pressure.
didHaveMemoryPressureclears the in-memory image cache; the disk copy stays. -
Concurrency locks for file I/O (
AnalyticsQueueLock). The analytics pipeline uses an isolate to write events topending.jsonl(track()) and another isolate to sync and delete the file (syncNow()). To prevent race conditions where an event is lost if written during a sync deletion, a mutex lock ensures only one process accesses the file at a time, without blocking the asynchronous network upload.
| Type | What is stored | Tool | Feature | Author |
|---|---|---|---|---|
| Local > Private > DB | Cached listings (cached_materials), queued listings (pending_listings), queued chat messages (pending_messages) |
Room (SQLite) + KSP | Offline Home, offline publishing, offline chat | Daniel Diab |
| Local > Private > DB | Queued check-ins, ratings and meeting proposals (pending_actions, payload as JSON) |
Room (SQLite) | Offline exchange steps | Daniel Diab |
| Local > Private > DB | Last server response per user and screen: chat threads, inbox, free times, meeting points, wishlist, notifications, alerts (cached_responses) |
Room (SQLite) + Gson | Offline chats and saved copies | Daniel Diab |
| Local > Private > Preferences | Session (JWT, user id, email, name) | Jetpack DataStore (session) |
Persistent login | Carla González Mina |
| Local > Private > Preferences | Unfinished listing draft as JSON | Jetpack DataStore (sell_draft) + Gson |
Sell draft auto-save | Daniel Diab |
| Local > Private > Files | Pending analytics events (pending.jsonl) |
App-specific files | Analytics pipeline | Daniel Diab |
| Local > Private > Files | Photos for listings being published offline (listing-photos) |
App-specific files (filesDir) |
Offline publishing with photos | Santiago Bernal |
| Local > Private > Cache | Downloaded photos, one file per URL (SHA-256 name), max 50 MB | cacheDir/images |
Photos offline | Daniel Diab |
We chose Room for data that is structured, queried and observed (lists of listings, messages and pending actions with states). For saved copies of server answers, cached_responses keeps one JSON row per user and screen: they are always read whole, so a table per screen would add no value. DataStore is for small key-value data that is read whole: the session and a single draft. A file is for an append-only log of events. Downloaded photos go to cacheDir, because Android may delete them when space is low and the app can always download them again.
The Room database went from version 3 to 4 with an explicit migration that creates cached_responses and pending_actions, so users who update the app keep their queued listings and messages.
| Type | What is stored | Tool | Feature | Author |
|---|---|---|---|---|
| Local > Private > DB | Queued chat messages (pending_messages), queued listings (pending_listings) |
sqflite (SQLite) | Offline chat, offline publishing | Sabrina Noguera (messages), Angie Gutiérrez (listings) |
| Local > Private > DB | Queued exchange confirmations, ratings and meeting proposals (pending_actions, added in database version 2, payload as JSON) |
sqflite | Offline exchange steps | Andrés Botero |
| Local > Private > DB | Last server response per user and screen: listings, chat threads, inbox, free times, meeting points, wishlist, notifications, profile (cached_responses) |
sqflite + dart:convert
|
Offline browsing, search, chats and saved copies | !!!!! |
| Local > Private > Preferences | Unfinished listing draft (sell_draft) |
shared_preferences | Sell draft auto-save | Angie Gutiérrez |
| Local > Private > Preferences | Registration draft without passwords (register_draft) |
shared_preferences | Account creation offline | Sabrina Noguera. |
| Local > Private > Preferences | Rating draft (rating_draft:<userId>:<exchangeId>) |
shared_preferences | Rating offline | Andrés Botero. |
| Local > Private > Preferences |
allow_mobile_data option |
shared_preferences | Optional data use | Sabrina Noguera. |
| Local > Private > Preferences (encrypted) | Session token (JWT) | flutter_secure_storage (already used) | Persistent login | !!!! |
| Local > Private > Files | Photos of listings being published offline (pending_photos/) |
path_provider (documents folder) |
Offline publishing with photos | Angie Gutiérrez |
| Local > Private > Files | Pending analytics events (pending.jsonl) |
path_provider |
Analytics pipeline. | Angie Gutiérrez |
| Local > Private > Cache | Downloaded photos, one file per URL |
cached_network_image (cache folder) |
Photos offline | Sabrina Noguera |
We chose sqflite for data that is structured, queried, and has states (messages and pending actions with attempts and failure reasons). For saved copies of server answers, cached_responses keeps one JSON row per user and screen: they are always read whole, so a table per screen would add no value. shared_preferences is for small key-value data read whole: drafts and the data option. The token stays in flutter_secure_storage because it is a credential and is encrypted by the OS keystore, and drafts never contain passwords. Files are for photos (binary data does not belong in the database) and for an append-only log of events. Downloaded photos go to the cache folder because the OS may delete them when space is low and the app can always download them again, whereas queued photos go to the documents folder because they cannot be recovered.
| # | Feature | What it does | Files | Author |
|---|---|---|---|---|
| 1 | Account registration | Name, major, institutional email (.edu / .edu.co), password with confirmation, against POST /auth/register. Errors such as "email already taken" appear in the form, and a successful registration starts the session. |
screens/auth/RegisterScreen, data/auth/AuthRepository, data/remote/AuthApi, navigation/CampusSwapApp
|
Daniel Diab |
| 2 | Input validation and limits | Shared rules for every form: names letters-only, emails without spaces, passwords 8–72 characters with letters and numbers, prices $1.000–$5.000.000 COP formatted while typing, title and description minimums with counters, length limits in search, alerts, address and chat, max 5 units per cart item, address required for home delivery. |
domain/InputValidation, components/Formatting, screens/sell/SellScreen, screens/cart/*, screens/search/SearchScreen, screens/home/AlertsScreen
|
Daniel Diab |
| 3 | Copy/paste blocking | No copy, cut or paste menu in the auth and sell fields; passwords also reject pasting from the keyboard clipboard. |
components/NoClipboard, screens/auth/*, screens/sell/SellScreen
|
Daniel Diab |
| 4 | Filled bottom-bar icons | The active tab shows a filled icon and a bold label. |
components/CampusIcons, navigation/BottomNavBar, navigation/Destinations
|
Daniel Diab |
| 5 | Offline Home | Room cache of listings, offline banner showing the age of the saved copy, automatic refresh on reconnect. |
data/local/CampusSwapDatabase, data/materials/MaterialRepository, data/connectivity/ConnectivityObserver, components/OfflineBanner, screens/home/HomeScreen
|
Daniel Diab |
| 6 | Offline publishing | Outbox in Room plus OutboxWorker (WorkManager) that sends listings with POST /materials; "Waiting to publish" and "Not published" states with Discard. |
data/sync/*, data/local/CampusSwapDatabase, data/materials/MaterialRemoteDataSource, screens/sell/SellScreen, screens/product/ProductDetailScreen, components/ProductCard
|
Daniel Diab |
| 7 | Sell draft auto-save | The draft is saved to DataStore while typing and restored when the form is opened again. |
data/local/DraftStore, screens/sell/SellScreen
|
Daniel Diab |
| 8 | Real chat with the backend | Replaces the simulated chat: messages and meeting proposals come from /chatrooms and /messages, the open thread refreshes by polling, and messages are sent through the offline outbox. |
data/ChatRepository, data/ChatRoomRepository, data/remote/ChatRoomsApi, data/remote/MessagesApi, screens/chat/ChatViewModel, screens/chat/ChatScreen
|
Carla González Mina |
| 9 | Conversations inbox | List of the user's conversations with the last message and unread count, so sellers can open and answer conversations. |
data/ConversationsRepository, screens/chat/ConversationsViewModel, screens/chat/MessagesScreen
|
Carla González Mina |
| 10 | Photos in the chat | Send a photo from the camera or the gallery; it is uploaded with POST /uploads and shown in the thread. |
data/remote/UploadsApi, components/RemoteImage, screens/chat/ChatScreen, res/xml/file_paths.xml
|
Carla González Mina |
| 11 | Campus map | The meeting point screen draws the real campus, with buildings, streets, paths and green areas. |
screens/meeting/CampusGeometry, screens/meeting/CampusMapProjection, res/raw/campus_buildings.json, screens/meeting/MeetingPointScreen
|
Carla González Mina |
| 12 | Shared meeting point | The other person is anchored to a meeting point both sides can see, and the screen explains the point that is on screen. |
data/location/CounterpartLocationSource, screens/meeting/MeetingPointViewModel, domain/MeetingPointRanking
|
Carla González Mina |
| 13 | Contact-seller event | The first chat message is reported to the backend as contacting the seller, completing the data for BQ2. |
analytics/AnalyticsEvent, analytics/BackendEventMapper, screens/chat/ChatViewModel
|
Carla González Mina |
| 14 | Offline chats and inbox | Every chat thread, the inbox and the counterpart's name are saved per user, and shown offline with their age; saved copies never mark messages as read. |
data/local/ResponseCache, data/ChatRepository, data/ConversationsRepository, screens/chat/*
|
Daniel Diab |
| 15 | Offline exchange steps | Check-in, rating and meeting proposal are queued in pending_actions and sent in order by the outbox; the screens change to confirmed by themselves when they are delivered. Free times are saved so a meeting can be proposed offline. |
data/sync/PendingActions, data/sync/OutboxSync, data/sync/OutboxRepository, screens/rating/*, screens/meeting/MeetingPointViewModel, data/MeetingProposalRepository
|
Daniel Diab |
| 16 | Two-level photo cache | Memory LruCache plus a 50 MB disk cache, one shared download per photo, 1080 px reduction and 10 s timeouts. Used by listings and chat photos. |
data/local/ImageCache, data/local/ImageDiskCache, components/RemoteImage, CampusSwapApplication
|
Daniel Diab |
| 17 | Server-side Smart Matching | Alerts are saved on the backend (/smart-alerts). When a listing is published, the backend sends one SMART_MATCH notification to each student whose alert it matches (never to the seller). The app no longer computes match notifications on the phone. |
App: data/alerts/*, screens/home/AlertsScreen, data/AppViewModel. Backend: src/smart-alerts/*, src/materials/materials.service, Prisma model SmartAlert
|
Daniel Diab |
| 18 | Meeting confirmation event (BQ4) |
MEETING_CONFIRMED is reported once, when the proposal is accepted, with its chatRoomId, so the analytics backend can count the messages and time before the agreement. The backend enum was extended to accept it. |
screens/chat/ChatViewModel, analytics/BackendEventMapper, backend prisma/schema.prisma
|
Daniel Diab |
| 19 | Offline wishlist and notifications | Both lists are saved per user and shown after a restart without a connection; notifications also load in the background on log-in and reconnect, so the unread badge is up to date. |
data/wishlist/WishlistRepository, data/notifications/NotificationRepository, navigation/CampusSwapApp
|
Daniel Diab |
| # | Feature | What it does | Files | Author | Status |
|---|---|---|---|---|---|
| 1 | Local database and response cache | sqflite database with pending_messages, pending_listings and cached_responses. Every successful server answer is saved per user with the time it was saved, so screens can show it later with its age. Includes an onUpgrade hook for future schema changes. |
lib/model/data_sources/local/app_database.dart, lib/model/data_sources/local/response_cache.dart
|
Sabrina Noguera | ✅ Implemented |
| 2 | LRU memory cache |
LruCache<K,V> built on a LinkedHashMap with a fixed capacity: a read moves the entry to the end, and when the capacity is exceeded the first (least recently used) entry is removed. It sits in front of the sqflite cache, so reopening Home or a chat is instant. Only the memory copy is evicted. |
lib/model/data_sources/local/lru_cache.dart, lib/model/data_sources/local/response_cache.dart
|
Sabrina Noguera | ✅ Implemented |
| 3 | Offline Home | Home saves each successful listings response. Without a connection it shows the saved listings, an offline banner with the age of the copy, and refreshes by itself when the connection returns. On a first launch offline it shows "Connect once to load listings" with a Retry button. |
lib/view/screens/home_screen.dart, lib/view_model/marketplace_view_model.dart, lib/model/repositories/impl/marketplace_repositories_impl.dart, lib/view/widgets/offline_banner.dart
|
Sabrina Noguera | ✅ Implemented |
| 4 | Outbox and sync service | Messages and listings are written to the local database first and sent later. The sync service flushes when the connection returns, when the app resumes and on a retry timer with backoff, one flush at a time. Each item ends as sent, retry later or refused with the server's reason. Rows belong to one user. The flush is launched as a Future with .then().catchError() handlers. |
lib/model/data_sources/local/outbox_store.dart, lib/model/services/outbox_sync_service.dart, lib/model/data_sources/remote_outbox_sender.dart, lib/main.dart
|
Sabrina Noguera | ✅ Implemented |
| 5 | Offline chat | A message appears at once with a clock icon, waits in the outbox and is sent in order; it then shows a tick. A Stream<int> of pending messages feeds the chat. Polling is paused while offline. |
lib/view_model/chat_view_model.dart, lib/view/screens/chat_view.dart, lib/model/use_cases/pending_message_use_cases.dart
|
Sabrina Noguera | ✅ Implemented |
| 6 | Saved chats and inbox | Each thread and the inbox are saved per user and shown offline with their age, followed by the messages still waiting to be sent. A saved copy never marks messages as read. |
lib/view/screens/messages_screen.dart, lib/view_model/chat_view_model.dart
|
Sabrina Noguera | ✅ Implemented |
| 7 | Sell draft auto-save | The new-listing form is saved to preferences 0.4 s after the user stops typing, and restored (with its photos) when the form is opened again. It is deleted once the listing is published or queued. |
lib/model/repositories/impl/draft_repositories_impl.dart, lib/model/data_sources/local/draft_photo_store.dart, lib/view_model/smart_listing_view_model.dart, lib/view/screens/new_listing_smart_screen.dart
|
Angie Gutiérrez | ✅ Implemented |
| 8 | Offline publishing | If publishing fails without a connection, the listing is saved in the local database and its photos in a local folder. The photos are compressed in an isolate (Isolate.run) and written to a temporary file that is then renamed. The listing shows as "Waiting to publish" in My Listings, and "Not published" with Discard if the server refuses it. |
lib/model/repositories/impl/pending_listing_repository_impl.dart, lib/core/isolates/photo_compressor.dart, lib/view_model/smart_listing_view_model.dart, lib/view/screens/seller_hub_screen.dart
|
Angie Gutiérrez | ✅ Implemented |
| 9 | Registration draft and offline messages | Name, major and institutional email are saved while typing, so they survive a closed app. Passwords are never saved. Login and registration show "Waiting for connection" and keep everything typed. |
lib/view_model/register_view_model.dart, lib/model/repositories/impl/draft_repositories_impl.dart, lib/view/screens/register_screen.dart, lib/view/screens/login_screen.dart
|
Sabrina Noguera | ✅ Implemented |
| 10 | Photo cache | Listing and chat photos are cached in memory and on disk (200 files, 30 days) with cached_network_image, with a category placeholder when a photo was never downloaded. This replaces the Image.network calls. |
lib/view/widgets/cached_photo.dart |
Sabrina Noguera | ✅ Implemented |
| 11 | BQ4 conversation insight on real events | The back-end creates a MEETING_CONFIRMED event when a meeting proposal is accepted. Messages keep the time they were written (clientCreatedAt). The analytics service computes messages and time to agreement on demand, shown on the BQ4 dashboard page and as the insight card in the chat. |
lib/view/widgets/conversation_insight_card.dart, lib/view_model/chat_view_model.dart, lib/model/repositories/impl/conversation_insight_repository_impl.dart
|
Angie Gutiérrez, Sabrina Noguera | ✅ Implemented |
| 12 | Optional mobile data | The connection state also reports Wi-Fi or mobile data. A switch in Profile (off by default) lets the sync use mobile data; with it off and no Wi-Fi, the items wait and a bar says why, with a "Send now" option. |
lib/Data/data_sources/connectivity_data_source.dart, lib/Presentation/State Management/connectivity_state.dart, lib/Presentation/Screens/profile_screen.dart, lib/Data/sync/outbox_sync_service.dart
|
Sabrina Noguera | ❌ Not implemented |
| 13 | Rating draft | The complete-exchange screen saves the checklist, received condition, stars, tags and review 0.4 s after the last change (Timer debounce) and when the screen closes, and restores them when it reopens. Per user and exchange; deleted once the rating is sent or queued. The screen logic moved to CompleteExchangeViewModel. |
lib/model/data_sources/local/draft_store.dart, lib/model/repositories/impl/draft_repositories_impl.dart, lib/view_model/complete_exchange_view_model.dart, lib/view/screens/complete_exchange_screen.dart
|
Andrés Botero | ✅ Implemented |
| 14 | Offline session start | The profile is saved at every sign-in. Offline at launch, the app opens with it instead of the login screen and keeps the token; the token is checked when the connection returns or the app resumes, and a 401 signs out. Quick Unlock also works offline for the fingerprint account. |
lib/model/repositories/impl/auth_repository_impl.dart, lib/model/use_cases/restore_session_use_case.dart, lib/view_model/app_view_model.dart
|
Andrés Botero | ✅ Implemented |
| 15 | Offline exchange steps | Confirming the exchange, rating and proposing a meeting are queued in pending_actions (database version 2, through the onUpgrade hook) when there is no connection, and sent by the sync service in order per exchange. A refused step holds back the rest and shows its reason with Discard; a proposal whose time passed is refused on the phone. Screens follow the outbox and change to confirmed by themselves. |
lib/model/repositories/impl/pending_action_repository_impl.dart, lib/model/use_cases/pending_action_use_cases.dart, lib/model/services/outbox_sync_service.dart, lib/view_model/complete_exchange_view_model.dart, lib/view_model/meeting_planner_view_model.dart, lib/view_model/account_view_model.dart
|
Andrés Botero | ✅ Implemented |
| 16 | Offline wishlist and notifications | Both lists use network falling back to cache, saved per user in cached_responses, and show a "copy saved 2h ago" banner offline. They load apart from the listings and orders, so they show even when those fail. The copy is refreshed after a change. |
lib/model/repositories/impl/marketplace_repositories_impl.dart, lib/view_model/marketplace_view_model.dart, lib/view_model/account_view_model.dart, lib/view/widgets/saved_copy_banner.dart
|
Andrés Botero | ✅ Implemented |
| 17 | Average time to mark an item as sold (BQ 11) | What is the average time (in days) it takes for a newly listed academic item to be marked as sold? files | Sabrina Noguera | ❌ Not implemented | |
| 18 | Offline analytics queue | Events are appended to pending.jsonl first and uploaded in batches, with their real time and a client UUID; the file being sent is swapped atomically, so new events keep being written. |
lib/model/services/analytics_sync_service.dart, lib/model/repositories/impl/analytics_event_repository_impl.dart, lib/model/use_cases/track_event_use_case.dart
|
Angie Gutiérrez | ✅ Implemented |
| 19 | Offline meeting points and free times | Saved copy, then bundled campus zones; free times without the slots already past. |
lib/model/repositories/impl/meetup_repositories_impl.dart, assets/campus_zones.json, lib/model/rules/free_slot_expiry.dart
|
Angie Gutiérrez | ✅ Implemented |
| 20 | Average ticket by condition (BQ13) | Endpoint joining Exchange and Material to calculate the global average ticket and grouped by condition. Displayed as a financial insight card in the Seller Hub. |
analytics/views.py, lib/view/widgets/average_ticket_card.dart, lib/view_model/average_ticket_view_model.dart
|
Angie Gutiérrez | ✅ Implemented |
| Author | Features owned |
|---|---|
| Daniel Diab | Account registration, input validation and limits, copy/paste blocking, filled bottom-bar icons, offline Home (Room cache, network monitor, banners), offline publishing (outbox and WorkManager), sell draft auto-save, offline chats and inbox, offline exchange steps (check-in, rating, proposal), two-level photo cache, server-side Smart Matching (app and backend), meeting confirmation event, offline wishlist and notifications |
| Carla González Mina | Real chat with the backend, conversations inbox, photos in the chat, campus map, shared meeting point, contact-seller analytics event |
| # | Feature | Files | Author | Status |
|---|---|---|---|---|
| 1 | Local database and response cache |
lib/model/data_sources/local/app_database.dart, lib/model/data_sources/local/response_cache.dart
|
Sabrina Noguera | ✅ Implemented |
| 2 | LRU memory cache | lib/model/data_sources/local/lru_cache.dart |
Sabrina Noguera | ✅ Implemented |
| 3 | Offline Home |
lib/view/screens/home_screen.dart, lib/view_model/marketplace_view_model.dart, lib/model/repositories/impl/marketplace_repositories_impl.dart, lib/view/widgets/offline_banner.dart
|
Sabrina Noguera | ✅ Implemented |
| 4 | Outbox and sync service |
lib/model/data_sources/local/outbox_store.dart, lib/model/services/outbox_sync_service.dart
|
Sabrina Noguera | ✅ Implemented |
| 5 | Offline chat |
lib/view_model/chat_view_model.dart, lib/view/screens/chat_view.dart, lib/model/use_cases/pending_message_use_cases.dart
|
Sabrina Noguera | ✅ Implemented |
| 6 | Saved chats and inbox |
lib/view/screens/messages_screen.dart, lib/view_model/chat_view_model.dart
|
Sabrina Noguera | ✅ Implemented |
| 7 | Sell draft and offline publishing |
lib/model/repositories/impl/draft_repositories_impl.dart, lib/model/data_sources/local/draft_photo_store.dart, lib/view_model/smart_listing_view_model.dart, lib/view/screens/new_listing_smart_screen.dart, lib/model/repositories/impl/pending_listing_repository_impl.dart, lib/core/isolates/photo_compressor.dart
|
Angie Gutiérrez | ✅ Implemented |
| 8 | Registration draft |
lib/view_model/register_view_model.dart, lib/model/repositories/impl/draft_repositories_impl.dart, lib/view/screens/register_screen.dart, lib/view/screens/login_screen.dart
|
Sabrina Noguera | ✅ Implemented |
| 9 | Photo cache | lib/view/widgets/cached_photo.dart |
Sabrina Noguera | ✅ Implemented |
| 10 | BQ4 insight and MEETING_CONFIRMED
|
lib/view/widgets/conversation_insight_card.dart, lib/view_model/chat_view_model.dart, lib/model/repositories/impl/conversation_insight_repository_impl.dart
|
Angie Gutiérrez, Sabrina Noguera | ✅ Implemented |
| 11 | Rating draft |
lib/view_model/complete_exchange_view_model.dart, lib/model/data_sources/local/draft_store.dart
|
Andrés Botero | ✅ Implemented |
| 12 | Offline session start | lib/model/repositories/impl/auth_repository_impl.dart |
Andrés Botero | ✅ Implemented |
| 13 | Offline exchange steps |
lib/model/repositories/impl/pending_action_repository_impl.dart, lib/model/services/outbox_sync_service.dart
|
Andrés Botero | ✅ Implemented |
| 14 | Offline wishlist and notifications |
lib/model/repositories/impl/marketplace_repositories_impl.dart, lib/view/widgets/saved_copy_banner.dart
|
Andrés Botero | ✅ Implemented |
| 15 | Offline analytics queue |
lib/model/services/analytics_sync_service.dart, lib/model/repositories/impl/analytics_event_repository_impl.dart, lib/model/use_cases/track_event_use_case.dart
|
Angie Gutiérrez | ✅ Implemented |
| 16 | Average time to mark an item as sold (BQ 11) | What is the average time (in days) it takes for a newly listed academic item to be marked as sold? - files | Sabrina Noguera | ❌ Not implemented |
| 17 | Average ticket by condition (BQ 14) |
analytics/views.py, lib/view/widgets/average_ticket_card.dart, lib/view_model/average_ticket_view_model.dart
|
Angie Gutiérrez | ✅ Implemented |
| 18 | Offline meeting points and free times |
lib/model/repositories/impl/meetup_repositories_impl.dart, assets/campus_zones.json, lib/model/rules/free_slot_expiry.dart
|
Angie Gutiérrez | ✅ Implemented |
PARA QUE ELIJAN SI QUIEREN FLUTTER
| # | Feature | What it does | Files | Author |
|---|---|---|---|---|
| 1 | Connectivity state and offline banners | Extends the existing connectivity state with the kind of connection, and shows the same offline banner in Home, chat, wishlist, and notifications (before, only Search had one); screens refresh when the connection returns. |
lib/Presentation/State Management/connectivity_state.dart, lib/Data/data_sources/connectivity_data_source.dart, lib/Presentation/Widgets/offline_banner.dart
|
!!!!! |