Final Submission ‐ Sprint 3 - Andres-acg-source/Moviles GitHub Wiki
Repositories and project tracking
| Item | Link |
|---|---|
| Android app (Flutter) | https://github.com/Rodri45/Moviles-Dart |
| iOS app (SwiftUI) | https://github.com/nbobadillah/Parkwise |
| Backend | https://github.com/Andres-acg-source/Moviles-Backend |
| Analytics dashboard | https://parkwise-api-a1f0.onrender.com/dashboard/ |
| Sprint 3 milestone (closed issues) | (link to the Sprint 3 milestone) |
| Kanban board | (link to the GitHub project) |
In this sprint each member implemented one new business question, different from the six of Sprint 2 and taken from the list we defined in Sprint 1. On Android the questions are about the reservation flow, the use of the forecast and the drivers we send to nearby lots. On iOS they are about the forecast shown to the driver, the speed and stability of the reservation flow, and the accessible and charging spots.
All twelve questions, the six of Sprint 2 and the six of this sprint, are answered in one dashboard that reads the same database the apps use (section 2).
| ID | Business Question | Type | In Sprint 1? | Justification | Owner | Evidence |
|---|---|---|---|---|---|---|
| BQ7 | Among users who check live availability, what percentage completes a reservation, and at which step of the flow does the rest drop off? | Type 3 | Yes (Reservation Conversion Gap) | If many drivers look at the spots but few reserve, the reservation flow has a problem. The step where they leave tells us whether to remove a screen, change the confirm button or improve the search. | Rodrigo García |
availability_viewed, spot_selected, reserve_confirm_tapped and reserve_failed from Android, reservations table, BQ7 card |
| BQ8 | What proportion of sessions rely on the occupancy prediction, checked before going to the parking lot, versus only the live map on arrival, and does checking the prediction change whether the driver ends up parking? | Type 3 | Yes (Prediction Versus Live-Only Usage) | The forecast is the most expensive feature to keep: it needs weeks of snapshots and its own endpoints. If drivers who check it do not park more often than drivers who only look at the live map, we can simplify it. | Andrés Cáceres |
forecast_viewed from both apps, map_loaded and availability_viewed, reservations table, BQ8 card |
| BQ9 | When the campus is full, what share of the drivers we send to a nearby lot actually park there, and how many drivers does that represent per lot and time slot? | Type 4 | Yes (Off-Campus Redirect Value) | It measures the traffic ParkWise sends to each nearby lot, which is the basis to negotiate a referral agreement or a better rate. It is reported by lot and slot, never per driver. | Fabio Higuera |
lot_redirected and lot_arrival_confirmed from Android, BQ9 card |
| BQ10 | What is the predicted occupancy of each level (P1, P2 and P3) in the 15-minute slot of the arrival time the driver chooses? | Type 2 | Yes (Predicted Occupancy by Level) | The driver chooses a level before leaving. Seeing the three levels side by side for their own arrival time answers that directly. Comparing past predictions with what really happened tells us how much the driver can trust them. | Nicolas Bobadilla | Levels card on Home in iOS, GET /recommendations/level, forecast_viewed, BQ10 card |
| BQ11 | What is the response time of the reservation requests and the crash rate along the reservation flow, by device model and OS version? | Type 1 | Yes (Reservation Flow Latency and Crashes) | Reserving happens in a hurry, often near the gate where the signal is weak. A slow or crashing confirmation makes the driver lose the spot. | Julián Bohórquez |
reservation_request, reserve_opened and app_crash from iOS, BQ11 card |
| BQ12 | What proportion of the time is at least one accessible spot and at least one charging spot free, by level and time slot? | Type 2 | Yes (Specialized Spot Availability) | Drivers with reduced mobility or an electric car can use only a few spots per level. Knowing how often one is free at their arrival time helps them decide whether to come by car at all. | Felipe Sales | Free accessible and charging spots saved with every occupancy snapshot, Find Spot filters and the Accessible and EV screen in iOS, BQ12 card |
- BQ8 asks for sessions where the prediction was "checked before leaving home". The app cannot know where the driver is, so we count a day as planned when the driver opened the forecast or changed the arrival time before the first live map or spot search of that day.
- BQ9 was left out in Sprint 2 because we are not connected to any nearby lot. The Sprint 1 definition already allowed user-confirmed arrivals as the data source, and that is what we use now: after tapping Navigate, the app asks the driver once whether they parked in that lot.
- BQ11 talks about crashes. We do not use an external crash service, so the iOS app saves a mark when the Reserve screen opens and removes it when the screen closes or the app goes to the background. If the app opens and the mark is still there, the previous session ended inside the reservation flow without closing normally, and it is counted as a crash.
The pipeline of Sprint 2 stays the same: both apps send telemetry events, the backend records reservations, check-ins and occupancy snapshots, and everything goes to one PostgreSQL database. In this sprint we added the six new questions and a dashboard that shows all of them together.
flowchart TD
subgraph Collect["1. Collection (phone)"]
direction TB
Droid["Android app<br/>TelemetryClient"]
IOS["iOS app<br/>TelemetryService"]
Queue[("Offline queues<br/>events and actions")]
end
subgraph Ingest["2. Ingestion (Moviles-Backend)"]
direction TB
Tel["POST /telemetry"]
Ops["/reservations and /levels"]
Sim["Occupancy simulator<br/>snapshot every 15 min"]
end
subgraph Store["3. Storage (one PostgreSQL on Neon)"]
direction TB
Events[("telemetry_events")]
Res[("reservations")]
Snap[("occupancy_snapshots<br/>with free accessible and EV")]
end
subgraph Agg["4. Aggregation (one SQL query per BQ)"]
direction TB
S2["BQ1 to BQ6"]
S3["BQ7 to BQ12"]
end
subgraph Use["5. Consumption"]
direction TB
Dash["Team<br/>dashboard /dashboard/"]
Driver["Driver, inside the app<br/>BQ2, BQ6, BQ10, BQ12"]
end
Collect --> Ingest --> Store --> Agg --> Use
Diagram Rationale
- Collection: events produced without signal wait in the queue of each app and are sent when the connection returns, so the questions also count what happens underground.
- Storage: the only change to the database is two columns in the occupancy snapshots, the free accessible and the free charging spots of each level, which BQ12 needs.
- Aggregation: each question is one SQL query in its own file in
src/analytics/, andGET /api/v1/analytics/summaryputs them together. - Consumption: the Type 2 questions go back to the driver inside the app. All twelve are shown to the team in the dashboard.
- Privacy: the dashboard needs a session and only shows aggregated values. User ids never leave the backend.
All the business questions are in one dashboard at https://parkwise-api-a1f0.onrender.com/dashboard/. It is part of Moviles-Backend, so it reads the same PostgreSQL database that both apps use. There is no second database and no exported file.
- The page signs in with a ParkWise account and calls
GET /api/v1/analytics/summary. Each section of that answer is computed with SQL at the moment the page asks for it. - It refreshes every 30 seconds while the tab is visible, and there is a Refresh button.
- There is one card per question with its owner and type, and a table view under each chart.
- The charts are drawn with plain SVG served by the backend itself, so the security headers (
script-src 'self') did not have to be relaxed.
(screenshot of the dashboard)
| BQ | Owner | Type | What the card shows |
|---|---|---|---|
| BQ1 | Andrés Cáceres | Type 1 | Map load time and failure rate by device, OS version and level |
| BQ2 | Nicolas Bobadilla | Type 2 | Walking time views and average minutes, total and by level |
| BQ3 | Felipe Sales | Type 3 | Uses of each filter and share of active users who never used it |
| BQ4 | Julián Bohórquez | Type 4 | Availability queries with the campus full, by weekday, slot and zone |
| BQ5 | Fabio Higuera | Type * | No-show rate, recommended window and spots to hold by level and slot |
| BQ6 | Rodrigo García | Type 2 | Drivers with enough trips and their suggested time to reserve |
| BQ7 | Rodrigo García | Type 3 | Reservation funnel, step with the biggest drop and reasons of failed attempts |
| BQ8 | Andrés Cáceres | Type 3 | Planned and live-only days, and the share of each that ended parked |
| BQ9 | Fabio Higuera | Type 4 | Redirects per lot and slot and the share confirmed as parked |
| BQ10 | Nicolas Bobadilla | Type 2 | Predicted against real occupancy per level and how often drivers check it |
| BQ11 | Julián Bohórquez | Type 1 | Response time of reserve, I parked and I left, and crashes in the flow, by device and OS |
| BQ12 | Felipe Sales | Type 2 | How often an accessible and a charging spot are free, by level and hour |
| BQ | Data collected | How it is computed |
|---|---|---|
| BQ7 |
availability_viewed when Find a spot shows the spots, spot_selected, reserve_confirm_tapped, reserve_failed with its reason, and the reservations created by users |
The unit is one user on one Bogotá day. A step only counts if the previous one happened that day. The last step comes from the reservations table, so a lost or repeated event cannot change it. Count per step, share kept from the previous step, step with the biggest drop, reasons of the failures and the last 14 days |
| BQ8 |
forecast_viewed (platform, screen, arrivalSlot, source) when the driver opens the forecast or changes the arrival time, the live views of BQ1 and BQ7, and the reservations with check-in |
One user on one Bogotá day. The day is planned if the forecast was viewed before the first live view, and live only if not. It ended parked if a reservation of that day was checked in. Share of each group and parking rate of each group, by platform, last 28 days |
| BQ9 |
lot_redirected (lotId) when the driver taps Navigate, and lot_arrival_confirmed (lotId, parked, minutesAfter) when the driver answers the question |
Per lot and hour of the redirect: redirects, answers, confirmed as parked, and the share parked among the answers, last 28 days |
| BQ10 | Occupancy snapshots of the last four weeks, and forecast_viewed from iOS |
The prediction is the average occupancy of the same weekday and 15-minute slot in the four previous weeks. To check it, every snapshot of the last 7 days is compared with the prediction it had, and the average error is shown per level |
| BQ11 |
reservation_request (action, durationMs, success, status, deviceModel, osVersion), reserve_opened and app_crash (screen, deviceModel, osVersion) |
Median and 90th percentile of the response time per action, share of failed requests, and crashes per 100 openings of the Reserve screen, by device and OS version |
| BQ12 |
free_accessible and free_ev saved in every snapshot |
Per level and 15-minute slot, share of the snapshots of the last 28 days with at least one free accessible spot, and the same for charging spots. The app receives it from GET /special-spots/availability
|
ParkWise is used in the place with the worst signal on campus. P2 and P3 are underground and have no signal at all, and the ramps and the gate have a weak one. These are the scenarios both apps handle:
| ID | Scenario | Where it happens | What the app must do |
|---|---|---|---|
| EC1 | No signal at all | P2 and P3, and some corners of P1 | Keep working with what is on the phone and say clearly that it is not live |
| EC2 | The signal is lost in the middle of an action | Tapping "I parked" while driving down the ramp | Not lose the action and not apply it twice |
| EC3 | Weak or slow network | The ramps and the gate, where the phone shows signal but requests do not finish | Not freeze waiting. Requests have a time limit, and after three failures in a row the app stops trying for a moment and answers from the phone |
| EC4 | The app is opened without signal | The driver opens ParkWise already underground | Open with the saved session and data, not with an error screen |
| EC5 | The signal comes back | Leaving the parking lot | Send what is waiting, in order, and reload the screens by themselves |
| EC6 | The phone has internet but the server cannot be reached | The backend is restarting or waking up | Behave as in EC1 and say that the server could not be reached |
We do not use one generic message for the whole app. In Sprint 2 the iOS app showed the same banner on every screen when the connection was lost. In this sprint it was replaced: each view below has its own behavior and its own message, and the driver can keep navigating between views without signal.
These are the strategies we use, with the names we give them in the tables:
- Network, then saved copy: ask the server first, and if it cannot be reached show the last copy saved on the phone with the time it was saved.
- Saved copy first: show what is on the phone at once, and ask the server only if the copy is old.
- Prefetch: download what will be needed underground while there is still signal.
- Local queue: save the action on the phone and send it when the signal returns.
- Optimistic update: show the result of a queued action at once, as if the server had already accepted it.
- Online only, with a reason: for actions that only the server can decide, disable the button and explain why, keeping what the driver already chose.
- Local computation: compute on the phone what can be computed with saved data, such as filters, search and recommendations.
| View | Owner | Scenarios | What the app does | Strategy | Why |
|---|---|---|---|---|---|
| Login | Rodrigo García | EC1, EC4, EC5 | The button changes to "Waiting for connection" and a banner explains that signing in needs the server. What was typed stays in the form, and the button turns on by itself when the signal returns. With a saved session the app opens directly. | Online only, with a reason | A login cannot be queued: the password must not be stored and only the server can check it. Telling the driver why avoids repeated taps. |
| Register | Rodrigo García | EC1, EC2 | Name and email are saved on the phone while the driver types. Without signal the button is off with an explanation. When the screen opens again the form is filled with the draft and a banner says so. The password is never saved. | Local draft, online only | The driver does not have to type everything again, and no credential stays on the phone. |
| Find a spot | Rodrigo García | EC1, EC3 | The list comes from the last copy of the three levels, with its time. Filters, search and the recommended spot are computed on the phone, and the driver can still choose a spot and go to Reserve. | Network, then saved copy. Local computation | The driver can still decide where to go, and the time of the copy says how old it is. |
| Reserve | Rodrigo García | EC1, EC2, EC5 | "I parked" and "I left" are saved in a local queue and the screen shows the result at once. A banner says how many actions are waiting. The queue is sent in order when the signal returns. The countdown keeps running with the time set by the server. Creating a reservation needs the server, so the button is off and the chosen spot stays selected. | Local queue with optimistic update. Online only for new reservations | The check-in happens where there is no signal, so it cannot depend on it. A new reservation is different: two phones could take the same spot, so only the server can confirm it. |
| Home | Andrés Cáceres | EC1, EC4, EC5 | The level summary and the forecast of the day come from the phone, with the time they were saved. The recommended level for the chosen arrival time is computed on the phone from the saved forecast, so changing the time still works. Everything reloads by itself when the signal returns. | Network, then saved copy. Local computation | The forecast changes slowly, so a copy from the same morning is still useful to plan the arrival. |
| Level map | Andrés Cáceres | EC1, EC3, EC5 | The map shows the saved copy of the level with its time. The refresh every 5 seconds stops while there is no signal and starts again when it returns. The level chips mark which levels are saved on the phone, and a level that was never saved shows a message asking to open it once with signal. | Prefetch. Network, then saved copy | P2 and P3 are where the map matters most and where there is no signal. When Home opens with signal the three levels are downloaded, so they are already on the phone when the driver goes down. Asking every 5 seconds without signal only spends battery. |
| Forecast (new) | Andrés Cáceres | EC1, EC4 | Hour by hour forecast of the three levels for today with the arrival time selector. Without signal it reads the saved file and shows when it was saved. | Saved copy first. Local computation | The driver can plan the trip before leaving, even from a place with poor signal. |
| Destinations (new) | Andrés Cáceres | EC1 | The buildings and their photos come from the phone. A photo that was never downloaded shows the initials of the building. The choice is saved on the phone and used by the map and Find a spot. | Network, then saved copy. Image cache | The destination changes the walking minutes of every spot, so the driver must be able to change it anywhere. |
| No campus spots | Fabio Higuera | EC1, EC3, EC5 | The nearby lots come from the copy saved in SQLite, with their photos from the image cache. Navigate still opens Google Maps with the address, and the redirect is saved on the phone and sent later. Notify me pauses without signal, says so, and continues when the signal returns. | Saved copy first. Local queue | When the campus is full the driver is at the gate, in a hurry and with weak signal. The lots change rarely, so the saved copy is reliable. |
| Find my car | Fabio Higuera | EC1, EC4 | Level, spot and the position saved at check-in come from the phone when the backend cannot be reached. Distance and walking minutes come from the GPS, which does not need data. Underground the app uses the last known position and says so. A check-in still waiting in the queue already counts as parked. | Saved data first. Sensor without network | This view is used exactly underground, so it must not depend on the network. |
| Offline | Fabio Higuera | EC1, EC4, EC5 | Shows the saved summary of the levels, the open reservation, the saved map of its level and how many actions are waiting to be sent. It closes by itself when the signal returns. | Saved data first | It is the screen the driver reaches from Home without signal, and it puts together what still works. |
| Profile | Fabio Higuera | EC1 | The reservation history comes from SQLite with the label "Saved on this phone". Signing out without signal asks for confirmation when actions are waiting, because they are deleted with the session. The question of BQ9 can be answered without signal and is sent later. | Network, then saved copy. Local queue | The driver can check past reservations anywhere, and nothing is lost by surprise when signing out. |
| View | Owner | Scenarios | What the app does | Strategy | Why |
|---|---|---|---|---|---|
| Login | Nicolas Bobadilla | EC1, EC4, EC5 | The sign in button is disabled and a message explains that signing in needs the server. The typed email stays. "Not now" still works and opens the app as a guest with the saved data. | Online only, with a reason | Signing in needs the server, but looking at the parking does not need an account. |
| Register | Nicolas Bobadilla | EC1, EC2 | Name and email are saved on the phone while the driver types and come back when the screen opens again. The password is never saved. Without signal the button is disabled with the reason. | Local draft, online only | Same as Android: nothing has to be typed again and no credential stays on the phone. |
| Home | Nicolas Bobadilla | EC1, EC4, EC5 | The level summary, the forecast and the card with the three levels of BQ10 come from files saved on the phone, with their time. The recommended level for the chosen arrival time is computed on the phone from the saved forecast. | Network, then saved copy. Local computation | The driver plans before leaving, and the forecast of the morning is still valid without signal. |
| Parking map | Nicolas Bobadilla | EC1, EC3, EC5 | The map shows the saved copy with its time. The refresh loop stops without signal and starts again when it returns. The three levels are downloaded when Home loads with signal. | Prefetch. Network, then saved copy | Same reason as the Android map: the levels without signal are the ones the driver needs most. |
| Profile | Nicolas Bobadilla | EC1 | The signed in user comes from the saved session. Signing out works without signal, clears the Keychain and the data saved on the phone, and warns first if actions are waiting. | Saved data first | Signing out must always work, also on a shared phone with no signal. |
| Reserve spot | Julián Bohórquez | EC1, EC2, EC5 | Confirm is disabled without signal with the reason, and the chosen spot stays selected. "I parked" and "I left" are saved in a queue in SwiftData, the screen changes at once, and the actions are sent in order when the connection returns. The countdown uses the expiration time saved with the reservation. If the server refuses an action, the driver sees why. | Local queue with optimistic update. Online only for new reservations | Same reason as Android. Both apps use the same rules, so a reservation behaves the same on both platforms. |
| Campus full | Julián Bohórquez | EC1, EC3, EC5 | The nearby lots come from the saved file with their time and their photos from the image cache. Notify me checks the campus every 30 seconds only while there is signal and pauses otherwise. | Saved copy first | The driver is at the gate with weak signal. The alternatives must appear without waiting for the network. |
| Lot details (new) | Julián Bohórquez | EC1 | Photo, address, rate and walking minutes come from the phone. A photo that was never downloaded shows a placeholder. Navigate opens Apple Maps with the address. | Saved copy first. Image cache | The driver decides between lots by price and distance, and that information does not change during the day. |
| My reservations (new) | Julián Bohórquez | EC1 | The history comes from SwiftData with the label "Saved on this phone". A reservation with an action still in the queue is marked "Waiting to send". | Network, then saved copy | The driver can see past reservations without signal and knows which changes the server has not received yet. |
| Find spot | Felipe Sales | EC1, EC3 | The spots of the three levels come from the saved copy with its time. Filters, search and sorting by walking minutes are done on the phone. A result seen less than 30 seconds ago is shown at once from memory. | Network, then saved copy. Local computation | The driver can still choose a spot and see the walking minutes without signal. |
| Find car | Felipe Sales | EC1, EC4 | Level, spot and the position saved at check-in come from the phone. Underground the GPS uses the last known location and the screen says so, with the time the car was saved. | Saved data first. Sensor without network | Same as Android: this view is needed where there is no signal. |
| Offline (new) | Felipe Sales | EC1, EC4, EC5 | Shows the saved summary of the levels, the open reservation, where the car is and how many actions are waiting. It closes by itself when the connection returns. It replaces the generic banner of Sprint 2. | Saved data first | It is the same screen of the prototype and of the Android app, so both platforms behave the same without signal. |
| Accessible and EV spots (new) | Felipe Sales | EC1 | The availability of accessible and charging spots by level and slot comes from the saved file with its date. | Saved copy first | This data changes from week to week, so an old copy still helps the driver decide. |
Both apps follow the same sequence. The names are the Android ones.
sequenceDiagram
autonumber
actor Driver
participant VM as ReserveViewModel
participant Outbox as Offline-first repository
participant DB as SQLite on the phone
participant API as Moviles-Backend
Driver->>VM: I parked (no signal)
VM->>Outbox: checkIn(id)
Outbox->>API: POST /reservations/:id/check-in
API--xOutbox: network error
Outbox->>DB: save pending action
Outbox-->>VM: reservation shown as parked
VM-->>Driver: "Saved on this phone, 1 action waiting"
Note over Outbox: the signal returns, or the app opens again
Outbox->>DB: read pending actions in order
Outbox->>API: POST /reservations/:id/check-in
alt Accepted
API-->>Outbox: fulfilled
Outbox->>DB: delete the action
else The hold expired before it arrived (409)
API-->>Outbox: 409
Outbox->>DB: delete the action
Outbox-->>VM: message for the driver
end
Diagram Rationale
- The queue keeps the order of the actions. A driver can tap "I parked" and later "I left" without signal, and the server must receive them in that order.
- Only one sending runs at a time. The signal returning, the app opening and the Reserve screen loading can all ask to send at the same moment, and without that rule an action could be sent twice.
- A network error keeps the action in the queue. Any other answer from the server removes it, because sending it again would fail again, and the driver is told what happened.
(screenshots of each view without signal, Android and iOS)
| What | Where | Type | Owner | Why |
|---|---|---|---|---|
| Queue of "I parked" and "I left" made without signal | SQLite, table pending_actions in parkwise.db
|
Relational database | Rodrigo García | The queue needs an id that keeps the order, a delete by id when an action is sent and a count for the banner. Those are simple SQL operations. |
| Copy of the reservation history | SQLite, table reservation_history
|
Relational database | Rodrigo García | The history is read sorted by date and updated one row at a time. It is also what Profile shows without signal. |
| Nearby lots with the time they were saved | SQLite, table nearby_lots
|
Relational database | Fabio Higuera | The list is read sorted by walking minutes and replaced when it is older than one day. |
| Redirects waiting for the answer of the driver | SQLite, table lot_redirects
|
Relational database | Fabio Higuera | A redirect is created when the driver taps Navigate and closed hours later with the answer. It needs to be found by lot and date, which is a query on a table. |
| Forecast of the day | JSON file in the documents folder of the app | Local file | Andrés Cáceres | It is one document that is always read and written whole, once per morning. A file is enough and it can be read without opening any database. |
| Arrival time and last level opened | SharedPreferences | Preferences | Andrés Cáceres | Two small values that must be ready when Home and the map open. |
| Photos of buildings and lots | Folder of the image cache | Local files | Andrés Cáceres | The images are files, so they are saved as files, with an index that knows when each one was used. |
| Register draft | Hive, box drafts
|
Key-value | Rodrigo García | One small value per form, read synchronously when the screen opens. |
| Levels and spots, parked car and telemetry queue | Hive | Key-value | Sprint 2 | One copy per key, written every time the server answers. |
| Session token | flutter_secure_storage |
Encrypted preferences (Android Keystore) | Sprint 2 | The JWT is a credential, so it goes to encrypted storage and never to Hive or SQLite. |
The SQLite code is in lib/infrastructure/storage/, behind ports of the domain layer, so the ViewModels never use SQL directly. The tables of the nearby lots arrive in version 2 of parkwise.db, which upgrades the database of a phone that already had version 1.
| What | Where | Type | Owner | Why |
|---|---|---|---|---|
| Queue of "I parked" and "I left" made without signal | SwiftData, model PendingAction
|
Relational database (SQLite under SwiftData) | Julián Bohórquez | Same reasons as Android: order, delete by id and count. SwiftData is the Apple framework for this, so the app still has no external dependencies. |
| Copy of the reservation history | SwiftData, model StoredReservation
|
Relational database | Julián Bohórquez | Read sorted by date and joined with the pending actions to mark "Waiting to send". |
| Levels, spots, buildings, forecast, recommendation, nearby lots and availability of special spots | JSON files in Caches/ParkwiseCache (ResponseCache) |
Local files | Nicolas Bobadilla | Each answer of the server is one document with its date. In Sprint 2 only levels and spots were saved; now every answer that a view needs without signal is saved. |
| Photos of the nearby lots | Files in Caches/LotImages
|
Local files | Julián Bohórquez | The second level of the image cache: if the photo is not in memory, it is read from disk before going to the network. |
| Register draft, selected building, arrival time and last filter used | UserDefaults | Preferences | Nicolas Bobadilla, Felipe Sales | Small values read when a screen opens. The password is never saved here. |
| Mark of the reservation flow in progress | UserDefaults | Preferences | Julián Bohórquez | It is written when Reserve opens and removed when it closes or the app goes to the background. If it is still there when the app opens, the previous session crashed (BQ11). |
| Session token | Keychain | Keychain | Sprint 2 | The JWT is a credential, so it never goes to UserDefaults. |
| Parked car and telemetry queue | UserDefaults | Preferences | Sprint 2 | Small values that must survive closing the app. |
flowchart LR
subgraph Android["Android"]
direction TB
A1[("SQLite<br/>actions, history, lots, redirects")]
A2[("Hive<br/>copies, drafts, events")]
A3[("Files<br/>forecast, images")]
A4[("SharedPreferences<br/>arrival time, level")]
A5[("Secure storage<br/>JWT")]
end
subgraph IOS["iOS"]
direction TB
I1[("SwiftData<br/>actions, history")]
I2[("Files<br/>server copies, images")]
I3[("UserDefaults<br/>drafts, choices, events")]
I4[("Keychain<br/>JWT")]
end
Diagram Rationale
- Each kind of data goes where it fits: lists that are queried go to a relational database, whole documents go to files, small values go to preferences, and credentials go to encrypted storage.
- Both platforms keep the same data for the same reasons, so a view behaves the same on Android and iOS without signal.
| Strategy | Where | Owner | Why |
|---|---|---|---|
Isolate.run |
Find a spot. Filtering, searching, sorting the spots of the three levels and choosing the recommended spot run in a short-lived background isolate | Rodrigo García | The work runs again on every filter and every letter typed. In a background isolate the UI isolate only draws, and on a low-end phone it does not drop frames. The function is pure, so a test checks that it gives the same answer in any isolate. |
compute for decoding JSON |
Level map. The answer of the spots of a level is decoded in a background isolate | Andrés Cáceres | The map asks for its level every 5 seconds while it is open. Decoding the answer outside the UI isolate keeps the scroll of the grid smooth. |
| Future with handlers | Reserve, best time to reserve (leadTime().then(...).catchError(...)). Home, prefetch of the three levels, where each level has its own handler |
Rodrigo García, Andrés Cáceres | The advice and the prefetch are optional. The screen does not wait for them, and a level that fails does not cancel the other two. |
async / await
|
Reserve, confirm and check-in. No campus spots, loading the lots and sending the redirects | Rodrigo García, Fabio Higuera | These steps depend on the answer of the previous one, so the code waits for it and handles each error case in order. |
Future.wait |
Find a spot and Home ask for the three levels at the same time. Reserve loads the reservation, the history and the advice together | Rodrigo García, Andrés Cáceres | The requests do not depend on each other, so the screen waits for the slowest one instead of the sum. |
| Streams | Connectivity changes in every view. State of the queue, listened to by Reserve and Offline. GPS positions in Find my car. Refresh of the map as a periodic stream that pauses without signal | Rodrigo García, Fabio Higuera, Andrés Cáceres | The views react to changes instead of asking all the time. |
| One sending at a time | The queue keeps the Future of the sending in progress, and a second call gets the same one |
Rodrigo García | The signal returning, the app opening and the screen loading can ask to send at the same moment. Without this rule an action could be sent twice. |
| Search generations and wait after typing | Find a spot ignores the answer of an old search when a newer one started, and waits 150 ms after the last letter | Rodrigo García | Answers from the isolate can arrive out of order. The list on screen always matches the last search. |
Timer.periodic |
Notify me in No campus spots, every 30 seconds, stopped without signal | Fabio Higuera | The campus is checked only while the driver wants it and while it can actually be reached. |
| Strategy | Where | Owner | Why |
|---|---|---|---|
async let |
Home asks for the recommendation and the forecast of the three levels at the same time | Nicolas Bobadilla | In Sprint 2 the forecast waited for the recommendation, because it only showed the recommended level. Now Home shows the three levels, so the two requests are independent and Home waits for the slowest one instead of the sum. |
withTaskGroup |
Prefetch of the three levels when Home loads with signal. Find spot asks for the three levels at the same time | Nicolas Bobadilla, Felipe Sales | In Sprint 2 Find spot asked for the levels in a loop, one after the other, which tripled the wait with a weak signal. |
GCD, serial DispatchQueue
|
ResponseCache writes the files on its own background queue. NWPathMonitor reports the network on its own queue and the result is passed to the main actor |
Nicolas Bobadilla, Felipe Sales | Writing to disk never blocks the main thread, and the writes of the same file never mix. |
actor |
ReservationOutbox, the queue of "I parked" and "I left" |
Julián Bohórquez | The actor runs one call at a time, so two sendings cannot run together and an action cannot be sent twice. |
| Combine | The queue subscribes to the isConnected publisher of NetworkMonitor and sends when it changes to connected. Find spot waits 150 ms after the last letter typed with debounce
|
Julián Bohórquez, Felipe Sales | The app reacts to the network and to typing without polling. |
Task.detached |
Find spot filters and sorts the spots outside the main actor | Felipe Sales | The main actor only updates the list. Filtering and sorting do not use any view state, so they can run anywhere. |
Task cancellation with .task(id:)
|
The refresh loop of the Parking map and Notify me in Campus full | Nicolas Bobadilla, Julián Bohórquez | The loop stops when the driver leaves the view, changes level or loses the signal, so nothing keeps running in the background. |
Image decoding with byPreparingForDisplay()
|
Photos of the nearby lots | Julián Bohórquez | The image is decoded outside the main thread before it is shown, so the list does not stutter. |
LRU cache in memory for Find a spot (Rodrigo García, lib/core/cache/lru_cache.dart)
- Structure: a
LinkedHashMapthat keeps the keys in order of use. Reading an entry moves it to the end, so the first key is always the least recently used one, and it is the one removed when the cache is full. Reading and writing take constant time. - Key: destination building plus the selected filters. Value: the spots of the three levels with their time.
-
maxEntries = 16: there are four filters, so a destination has 2⁴ = 16 combinations, and all of them fit. -
ttl = 30 s: spots change in minutes. Within 30 seconds, turning a filter on and off shows the list at once without asking for the three levels again. After that the entry counts as a miss. - Only answers from the server go into memory. A copy read from disk without signal is never stored as if it were fresh, and pull to refresh skips the memory.
Image cache for the photos of buildings and lots (Andrés Cáceres, cached_network_image)
- One
CacheManagerfor the whole app, used by Destinations and No campus spots. -
stalePeriod = 30 days: the photos almost never change, so a saved photo is used for a month before asking for it again. -
maxNrOfCacheObjects = 50: there are four buildings and two lots today, so 50 leaves room for new places while keeping the folder small. When the limit is passed, the photos used least recently are deleted first. - Each photo is decoded at the size it is shown (
memCacheWidth), so a large file does not take a large amount of memory.
Prefetch and saved copies (Andrés Cáceres and Sprint 2)
- When Home loads with signal, the three levels are downloaded and saved, so the map of P2 and P3 is ready before the driver goes down.
- Every answer of levels and spots is saved in Hive with its time and used when the network fails, as in Sprint 2.
Nearby lots with a time to live (Fabio Higuera)
- No campus spots reads the lots from SQLite first and asks the backend only when the copy is older than 24 hours or when the driver pulls to refresh. The lots change rarely, so the screen usually opens without waiting for the network.
NSCache for the results of Find spot (Felipe Sales)
- Key: destination plus the selected filter. Value: the sorted spots with the time they were received.
-
countLimit = 16, for the same reason as Android: all the filter combinations of a destination fit. - Entries older than 30 seconds are ignored and asked again.
- NSCache frees memory by itself when the system needs it, so the app never has to clean it by hand.
Two-level image cache for the photos of the lots (Julián Bohórquez, LotImageCache)
- Memory:
NSCachewithcountLimit = 20andtotalCostLimit = 20 MB. The cost of each photo is its size in bytes once decoded, so a few large photos cannot fill the memory. - Disk: if the photo is not in memory it is read from
Caches/LotImages, and only if it is not there it is downloaded. The system may clean the Caches folder, which is fine for photos that can be downloaded again.
Saved answers and prefetch (Nicolas Bobadilla)
-
ResponseCachenow saves every answer that a view needs without signal, each one with its date, and the three levels are downloaded when Home loads with signal.
The occupancy of the spots is simulated by the backend. Reservations, check-ins, sessions, GPS positions and telemetry are real.
Both apps keep all the features of Sprint 2: login and register with JWT, live map refreshed every 5 seconds, Find a spot with filters and the recommended spot, reservation with countdown and check-in, Find my car with GPS, campus full with nearby lots, occupancy forecast and recommended level, profile with history, and the telemetry of BQ1 to BQ6. Since Sprint 2 the iOS app can also be used as a guest, and it asks for an account only to reserve, find the car or see the profile.
| Member | New in Sprint 3 |
|---|---|
| Rodrigo García | BQ7 and its events. Dashboard with all the questions. Login and Register without signal with the register draft. Find a spot with the ranking in a background isolate and the LRU cache. Reserve with the queue of actions in SQLite and the optimistic update. |
| Andrés Cáceres | BQ8 and forecast_viewed. Home and Forecast without signal from the saved file, with the recommended level computed on the phone. Prefetch of the three levels and the map paused without signal. New Forecast and Destinations views. Image cache for the photos. |
| Fabio Higuera | BQ9 with the question "Did you park here?". No campus spots with the lots in SQLite, photos and Notify me paused without signal. Find my car, Offline and Profile working from the data saved on the phone. |
Views (Android):
- Rodrigo García: Login, Register, Find a spot and Reserve.
- Andrés Cáceres: Home, Level map, Forecast (new) and Destinations (new).
- Fabio Higuera: No campus spots, Find my car, Offline and Profile.
| Member | New in Sprint 3 |
|---|---|
| Nicolas Bobadilla | BQ10 with the three levels card on Home. Login, Register, Home, Parking map and Profile without signal. Every answer saved on the phone with its date, prefetch of the levels and parallel loading on Home. |
| Julián Bohórquez | BQ11 with the response time and crashes of the reservation flow. Queue of actions in SwiftData behind an actor. Notify me in Campus full. New Lot details and My reservations views. Two-level image cache for the photos of the lots. |
| Felipe Sales | BQ12 with the availability of accessible and charging spots. Find spot with the levels loaded in parallel, filtering outside the main actor and NSCache. New Offline and Accessible and EV spots views. |
Views (iOS):
- Nicolas Bobadilla: Login, Register, Home, Parking map and Profile.
- Julián Bohórquez: Reserve spot, Campus full, Lot details (new) and My reservations (new).
- Felipe Sales: Find spot, Find car, Offline (new) and Accessible and EV spots (new).
- Dashboard at
/dashboard/with the twelve questions, served by the backend itself. - One SQL query per new question, BQ7 to BQ12, in
src/analytics/, added toGET /analytics/summary. - Free accessible and charging spots saved in every occupancy snapshot, and
GET /special-spots/availabilityfor the iOS app. - Photos of the buildings and the nearby lots served by the backend, taken by the team.
- Automated tests for every new question and for the dashboard.
- The iOS banner that appeared on every screen without signal was removed. Each view now has its own offline behavior, and the new Offline screen collects what still works.
- The answers of the business questions were only available as JSON in
GET /analytics/summary. Now they are all in one dashboard that reads the same database. - The work of this sprint is tracked in a Kanban board in GitHub Projects, linked to the issues and pull requests of the milestone.