Sprint 2 - Moviles-GN44/ISIS3510-GN44-wiki GitHub Wiki
| BQ | Type | Implemented by | Platform | Rationale |
|---|---|---|---|---|
| BQ5 – What is the completion rate of users scanning a QR code at a physical location to leave a review versus users searching manually to leave a review? | Type 2 | Felipe Boada | Flutter | Shows whether scanning the QR helps or hurts users finishing a review, since QR reviews are the most reliable data. |
| BQ3 – Which information do students use the most when comparing dining options: price, distance, estimated waiting time, menu availability, or dietary preferences? | Type 2 | Catalina Álvarez | Flutter | Helps identify which information students consult the most when comparing dining options, so the app can understand which criteria are more relevant during the decision process. |
| BQ6 – How much time on average does a student spend setting up filters such as price, distance, and dietary preferences before finally selecting a restaurant profile? | Type 2 | Juan José Ortiz | Flutter | Measures the friction students face when deciding where to eat with little time between classes. Comparing sessions with and without the "Suggested for you" filters shows whether the app makes that decision faster. |
| BQ9 – How often do students use the allergen and dietary restriction filters compared to the other filters in the app, and which restrictions are selected the most? | Type 3 | Juan José Ortiz | Flutter | Shows whether dietary filters are a feature students actually rely on when choosing where to eat, and which restrictions matter most, so the team can decide which dietary information to prioritize for each restaurant. |
The analytics pipeline connects user interactions in the Flutter application with the Business Questions defined for Sprint 2.
The general process is:
User interaction → Event generation → Cloud Firestore → Processing → Aggregation → Business Question result
flowchart TD
A[Student uses the Flutter app] --> B{User interaction}
B --> C[BQ3 - Restaurant comparison]
B --> D[BQ5 - Review flow]
B --> T[BQ9 / BQ6 - Explore filters]
C --> E[TelemetryService]
E --> F[restaurant_comparison_criterion]
F --> G[(Cloud Firestore - telemetry_events)]
G --> H[BQ3AnalyticsService]
H --> I[Filter BQ3 comparison events]
I --> J[Group by criterion]
J --> K1[Price]
J --> K2[Distance]
J --> K3[Waiting time]
J --> K4[Menu]
J --> K5[Dietary preferences]
K1 --> L[Count interactions]
K2 --> L
K3 --> L
K4 --> L
K5 --> L
L --> M[Calculate counts and percentages]
M --> N[Identify most consulted criterion]
N --> O[BQ3 result]
D --> P[Review telemetry events]
P --> G
G --> Q[Review funnel analysis]
Q --> R[Compare QR vs manual completion]
R --> S[BQ5 result]
T --> U[ExploreViewModel]
U --> E
E --> V[filter_applied]
E --> W[filter_session_completed]
V --> G
W --> G
G --> X[BQ9AnalyticsService]
X --> X0[Filter filter_applied events]
X0 --> X1[Count filter uses by type]
X1 --> X2[Dietary share vs other filters]
X1 --> X3[Most selected restriction]
X2 --> Y[BQ9 result]
X3 --> Y
G --> Z[BQ6AnalyticsService]
Z --> Z0[Filter filter_session_completed events]
Z0 --> Z1[Average and median time to decide]
Z0 --> Z2[Distribution by time range]
Z0 --> Z3[With vs without suggestion]
Z1 --> Z4[BQ6 result]
Z2 --> Z4
Z3 --> Z4
| Event | When | Params |
|---|---|---|
| review_flow_started | Scanner opens | – |
| review_method_selected | Valid QR scanned or restaurant picked manually | method: qr or manual |
| review_published | Review saved | method, verified |
BQ5 uses events from the review flow to identify whether the user followed the QR or manual path and whether the review was eventually published.
These events can be used to compare completion between the two methods.
BQ3 tracks interactions with information used by students while comparing restaurants.
These interactions are sent from the Flutter app to TelemetryService and stored in Cloud Firestore.
Collection flow:
User interaction → View / HomeViewModel → TelemetryService → Cloud Firestore (telemetry_events)
| Event | When | Params |
|---|---|---|
| restaurant_comparison_criterion | User checks the distance/location of a restaurant | criterion: distance, restaurantId |
| restaurant_comparison_criterion | User opens or consults the restaurant menu | criterion: menu, restaurantId |
| restaurant_comparison_criterion | User interacts with the alternative shown because of a high waiting-time context | criterion: waiting_time, restaurantId |
| restaurant_comparison_criterion | User consults restaurant menu information where prices are displayed | criterion: price, restaurantId |
| restaurant_comparison_criterion | User consults restaurant menu information where dietary tags are displayed | criterion: dietary_preferences, restaurantId |
The events are stored in the telemetry_events collection.
When an authenticated user is available, the user's UID can also be included in the telemetry event.
The BQ3 pipeline does not stop after collecting the telemetry events.
BQ3AnalyticsService processes the stored information to obtain results for the Business Question.
Processing flow:
telemetry_events → Filter BQ3 events → Group by criterion → Count interactions → Calculate percentages → Identify most consulted criterion
The service considers the five BQ3 criteria:
- Price
- Distance
- Waiting time
- Menu availability
- Dietary preferences
BQ3AnalyticsService reads events where:
eventName = restaurant_comparison_criterion
It then reads the criterion stored inside the event parameters and counts the interactions corresponding to each of the five supported criteria.
The service provides:
- Interaction count for each criterion.
- Total number of comparison interactions.
- Percentage represented by each criterion.
- Most consulted criterion.
Unknown criteria are ignored.
This transforms the collected telemetry into aggregated information that can be used to answer BQ3.
The interaction frequency is used as an indicator of which information students consult the most during the restaurant comparison process. It does not by itself prove that a criterion was the final reason for choosing a restaurant.
BQ9 tracks which filters students apply on the Explore screen.
Collection flow:
Apply Filters → ExploreViewModel → TelemetryService → Cloud Firestore (telemetry_events)
Each tap on Apply Filters sends one filter_applied event per active filter. Events from the same search share an applyId. A search without filters is sent once with filterType: none.
| Event | When | Params |
|---|---|---|
| filter_applied | User applies a dietary filter | filterType: dietary, value: vegan / gluten_free / lactose_free, applyId, filtersCount, userId |
| filter_applied | User applies a walking time limit | filterType: walking_time, value: 5 / 10 / 15, applyId, filtersCount, userId |
| filter_applied | User applies a budget range | filterType: price, value: min-max (e.g. 0-25000), applyId, filtersCount, userId |
| filter_applied | User applies a payment method | filterType: payment, value: Cash / Cards / Nequi, applyId, filtersCount, userId |
| filter_applied | User applies a category | filterType: category, value: Fast Food / Executive Lunch, applyId, filtersCount, userId |
| filter_applied | User applies no filters | filterType: none, value: none, applyId, filtersCount: 0 |
BQ9AnalyticsService reads events where eventName = filter_applied and computes:
- Number of uses of each filter type.
- Share of all filter uses that are dietary filters.
- Share of searches that used at least one dietary filter (grouping events by
applyId). - Most used filter type.
- Most selected dietary restriction.
BQ6 measures how long a student spends setting filters before choosing a restaurant.
A session starts with the first interaction on the Explore screen (changing a filter, applying, or using the suggestion) and ends when the student opens a restaurant from the results.
| Event | When | Params |
|---|---|---|
| filter_session_completed | User opens a restaurant from the Explore results | durationMs, filtersCount, filterChanges, searches, usedSuggestion, restaurantId, userId |
BQ6AnalyticsService reads events where eventName = filter_session_completed and computes:
- Average and median time to decide.
- Distribution of sessions: < 5 s, 5–10 s, 10–20 s, > 20 s.
- Average time with and without the "Suggested for you" filters.
- Average number of filter changes per session.
flowchart TB
subgraph VIEW["VIEW"]
direction LR
v1[Home / Map]
v2[Explore]
v3[Restaurant detail]
v4[QR scanner]
v5[Write review]
v6[Login / Register]
v7[Profile]
v7[Profile]
end
subgraph VIEWMODEL["VIEWMODEL"]
direction LR
vm1[HomeViewModel]
vm2[CampusMapViewModel]
vm3[ExploreViewModel]
vm4[ScanQrViewModel]
vm5[WriteReviewViewModel]
vm6[AuthViewModel]
vm7[ProfileViewModel]
end
subgraph MODEL["MODEL"]
direction TB
subgraph REPO["Repositories"]
direction LR
r1[RestaurantRepository]
r2[CampusRestaurantsRepository]
r3[ReviewRepository]
r4[AuthRepository]
r5[UserProfileRepository]
end
subgraph SERV["Services"]
direction LR
s1[UserLocationTracker]
s2[TelemetryService]
s3[Analytics services]
s4[Recommendation strategies]
end
subgraph ENT["Entities"]
direction LR
m1[Restaurant]
m2[Review]
m3[AppUser]
m4[RestaurantFilters]
end
end
subgraph EXT["BACKEND AND DEVICE"]
direction LR
e1[(Firebase Auth)]
e2[(Cloud Firestore)]
e3[OpenStreetMap]
e4[GPS · Camera · Light sensor]
end
VIEW -- "user actions" --> VIEWMODEL
VIEWMODEL -- "notifyListeners()" --> VIEW
VIEWMODEL --> MODEL
MODEL --> EXT
Rationale
The Flutter app follows MVVM with a Repository layer, and uses Firebase as its backend.
- View: the screens (Home/Map, Explore, Restaurant detail, QR scanner, Write review, Login/Register, Profile). They only draw the current state and send the user's actions to their ViewModel. They never access Firebase directly.
-
ViewModel: one per screen. It holds the screen state and its logic (selected restaurant, filters, location, loading and errors) and notifies the View when something changes through
ChangeNotifier. Because it does not depend on widgets, it can be tested without the UI. -
Model: the data and logic shared by the app.
- Repositories hide where the data comes from (Firestore, Firebase Auth) and return the app's own types, so moving from mock data to Firestore did not change any screen.
-
Services hold logic used by several screens: location (
UserLocationTracker), telemetry, analytics and recommendation strategies. This avoids duplicating it in each ViewModel. -
Entities (
Restaurant,Review,AppUser,RestaurantFilters) are the data types every layer shares.
-
Backend and device: Firebase is shared with the Kotlin app (Auth for
@uniandes.edu.coaccounts; Firestore for restaurants, reviews, user profiles and analytics events). OpenStreetMap provides the campus map, and the phone sensors provide location, photos and light.
Why this structure: each layer only talks to the next one. Members could work on different screens at the same time without conflicts, a screen can change without touching the data, and the data source can change without touching the screens.
Catalina contributed to the MVVM architecture through the restaurant comparison and recommendation flow.
Flow:
HomeScreen → HomeViewModel → RestaurantRepository / TelemetryService → Data and external services
HomeViewModel manages the selected restaurant, waiting-time context, alternative recommendation and BQ3 telemetry.
Restaurant data and recommendations are delegated to RestaurantRepository, while analytics events are delegated to TelemetryService.
This keeps UI responsibilities separated from state management, recommendation logic and external data access.
sequenceDiagram
actor S as Student
participant V as View
participant VM as ViewModel
participant LT as UserLocationTracker
participant R as Repository
participant T as TelemetryService
participant F as Cloud Firestore
Note over V,F: Opening the app
VM->>R: watchRestaurants()
R->>F: restaurants.snapshots()
LT->>LT: GPS strategy (fallback: building)
F-->>R: restaurant documents
R-->>VM: List<Restaurant>
LT-->>VM: location changed
VM->>VM: measure distances from the student
VM-->>V: notifyListeners()
V->>V: redraw map and "Near you"
Note over S,F: Filtering on Explore
S->>V: Apply Filters
V->>VM: applyFilters()
VM->>VM: filter and sort results
VM-->>V: notifyListeners()
VM-)T: filter_applied (BQ9)
T-)F: telemetry_events
S->>V: open a restaurant
V->>VM: openRestaurant()
VM-)T: filter_session_completed (BQ6)
T-)F: telemetry_events
Note over S,F: Publishing a review
S->>V: Publish
V->>VM: publish()
VM->>R: addReview()
R->>F: reviews.add() + update restaurant rating
F-->>R: saved (or queued offline)
VM-)T: review_published (BQ5)
VM-->>V: notifyListeners()
F-->>R: new rating via snapshots()
R-->>VM: updated restaurants
flowchart LR
MAPV["<b>Map / Home screen</b>"]
EXV["<b>Explore screen</b>"]
REVV["<b>QR + Review screens</b>"]
AUV["<b>Login + Profile screens</b>"]
HVM["<b>HomeViewModel</b><br/>CampusMapViewModel"]
EVM["<b>ExploreViewModel</b>"]
RVM["<b>ScanQrViewModel</b><br/>WriteReviewViewModel"]
AVM["<b>AuthViewModel</b><br/>ProfileViewModel"]
LOC["<b>UserLocationTracker</b><br/>GPS / Building strategy"]
TEL["<b>TelemetryService</b>"]
REPO["<b>Repositories</b><br/>Restaurant · Review · Auth · UserProfile"]
FS[("<b>Cloud Firestore</b>")]
FA[("<b>Firebase Auth</b>")]
GPS["<b>GPS sensor</b>"]
MAPV -- "method call" --> HVM
EXV -- "method call" --> EVM
REVV -- "method call" --> RVM
AUV -- "method call" --> AVM
HVM -. "notifyListeners()" .-> MAPV
EVM -. "notifyListeners()" .-> EXV
RVM -. "notifyListeners()" .-> REVV
AVM -. "notifyListeners()" .-> AUV
HVM -- "Future / Stream" --> REPO
EVM -- "Stream" --> REPO
RVM -- "Future" --> REPO
AVM -- "Future / Stream" --> REPO
LOC -. "location changed" .-> HVM
LOC -. "location changed" .-> EVM
GPS -- "position Stream" --> LOC
HVM -- "fire-and-forget" --> TEL
EVM -- "fire-and-forget" --> TEL
RVM -- "fire-and-forget" --> TEL
REPO -- "snapshots() / get / set" --> FS
REPO -- "authStateChanges()" --> FA
TEL -- "telemetry_events" --> FS
Rationale
Solid arrows are requests (one component asks another to do something); dotted arrows are notifications (one component tells another that something changed).
-
View → ViewModel (method call): each screen calls its ViewModel when the user acts, for example
selectRestaurant,applyFilters,publishorsignIn. Keeping the call in one direction means the screen has no business logic. -
ViewModel → View (
notifyListeners): ViewModels areChangeNotifiers and the screens redraw throughListenableBuilder(Observer pattern). A ViewModel does not know which widgets use it, so several views can follow the same state; for example, the map and the restaurant preview both followHomeViewModel. -
ViewModel → Repository (
Future/Stream): one-time operations return aFuture(sign in, publish a review, save the preferred building). Data that changes over time arrives as aStream: restaurants through Firestoresnapshots()and the session throughauthStateChanges(). This makes the app reactive: a new rating or a sign-in reaches every open screen without reloading. -
Shared location:
UserLocationTrackerreads the GPS position stream, or a campus building when the GPS is not available, and notifies both the map and Explore. Both screens measure distances from the same place without duplicating location logic. -
Telemetry (fire-and-forget): ViewModels send BQ events to
TelemetryServicewithout waiting for a response, so analytics never slows down or blocks the user. -
Firebase access in one layer: only the repositories and
TelemetryServicetalk to Firestore and Firebase Auth. Firestore's local cache answers reads and queues writes when there is no connection, and syncs them when it returns, so the app keeps working offline.
| Category | Pattern | Where | Responsible | Why |
|---|---|---|---|---|
| Creational | Singleton | AuthRepository.instance | Felipe | The app has exactly one user session and one instance with a global access point. |
| Structural | Adapter | AuthRepository and ReviewRepository | Felipe | Firebase returns its own types and the repositories convert them into the app's types, so ViewModels can work with the data. |
| Behavioral | Observer | ChangeNotifier + ListenableBuilder and authStateChanges() stream | Felipe | Views subscribe to ViewModels and redraw when notified and AuthViewModel observes the session to react to sign-in and sign-out. |
| Behavioral | Strategy | RestaurantRecommendationStrategy + FasterRestaurantStrategy + SmartRestaurantStrategy | Catalina | The recommendation algorithm is encapsulated behind a common strategy interface. The repository can use different recommendation algorithms without changing the UI or repository flow. SmartRestaurantStrategy ranks alternatives using waiting time, distance, rating and average menu price. |
| Behavioral | Strategy | LocationStrategy + GpsLocationStrategy + BuildingLocationStrategy, used by UserLocationTracker | Juan José | Knowing where the student is can be done in different ways. The GPS strategy reads the phone sensor; the building strategy places the student at a campus building. The tracker starts with the GPS and switches at runtime to a building when the GPS fails (no permission, location off, low accuracy or a position outside campus), or when the student picks one. The map, the Explore screen and the distance calculation only receive coordinates and never depend on how they were obtained. |
| Quality attribute | Tactic | Where | Responsible |
|---|---|---|---|
| Availability | Failover to local storage | Reviews published without connection are stored by Firestore on the device and synced when the connection returns | Felipe |
| Performance | Caching | The local cache answers review reads when offline | Felipe |
| Performance | Asynchronous processing | Firebase calls and telemetry run asynchronously | Felipe |
| Security | Authentication | Only @uniandes.edu.co accounts can publish reviews | Felipe |
| Security | Input validation | QR payload must be an existing restaurant | Felipe |
| Maintainability | Modularization | Features split into View / ViewModel / Repository layers | Felipe |
| Maintainability | Separation of concerns | BQ3 interaction handling is separated between the View, HomeViewModel and TelemetryService instead of placing analytics logic directly in the UI | Catalina |
| Performance | Asynchronous processing | BQ3 telemetry events are sent asynchronously to Firestore without placing the database operation directly in the UI | Catalina |
| Maintainability | Strategy-based recommendation | Restaurant recommendation algorithms are separated from the repository through RestaurantRecommendationStrategy | Catalina |
| Maintainability | Analytics separation | BQ3 aggregation is handled by BQ3AnalyticsService instead of mixing analytics processing with telemetry collection or UI logic | Catalina |
| Availability | Graceful degradation | When the GPS is unavailable, the location falls back to the student's preferred building (or ML) instead of failing | Juan José |
| Reliability | Input filtering | GPS readings with more than 50 m of error and positions outside campus are discarded so the location does not jump | Juan José |
| Performance | Caching | Restaurants come from a real-time Firestore listener with local cache, so the map and Explore keep working offline | Juan José |
| Usability | Scope limitation | The map camera is constrained to the campus bounds, keeping the app focused on nearby options | Juan José |
| Maintainability | Shared location service | UserLocationTracker is shared by the map and Explore instead of duplicating location and distance logic | Juan José |
| Maintainability | Analytics separation | BQ9 and BQ6 aggregation live in BQ9AnalyticsService and BQ6AnalyticsService, separate from collection and UI | Juan José |
| Item | Feature |
|---|---|
| a. Sensor | QR scanner with real camera and flashlight to verify the visit |
| b. Type 2 BQ | QR vs manual review funnel (BQ5) |
| c. Context aware | Ambient light sensor suggests the flashlight under 15 lux |
| d. Smart | Wait time estimated from student reviews |
| e. Authentication | Firebase email login/sign-up (@uniandes.edu.co), guest mode |
| f. External service | Reviews saved to and read from Cloud Firestore |
| Item | Feature |
|---|---|
| a. Sensor | Camera access for adding a dish photo to a restaurant review, including photo selection, preview and removal before publishing |
| b. Type 2 BQ | BQ3 comparison analytics to collect and aggregate the information students consult when comparing dining options |
| c. Context aware | Detects a high waiting-time context using current/reported waiting information and offers the user a restaurant alternative |
| d. Smart | Ranks restaurant alternatives using waiting time, walking distance, rating and average menu price |
| e. Authentication | Associates BQ3 comparison telemetry with the currently authenticated user's UID |
| f. External service | Sends BQ3 comparison telemetry to the telemetry_events collection in Cloud Firestore |
BQ3: Which information do students use the most when comparing dining options: price, distance, estimated waiting time, menu availability, or dietary preferences?
The implementation covers both the collection and processing sides of the BQ3 analytics pipeline.
The five comparison criteria covered are:
- Price
- Distance
- Estimated waiting time
- Menu availability
- Dietary preferences
When a comparison interaction occurs, the app records a restaurant_comparison_criterion event through TelemetryService.
The events are stored in the telemetry_events collection in Firestore.
BQ3AnalyticsService then processes these events by grouping them according to their comparison criterion.
For every criterion, the service can obtain:
- Number of interactions.
- Percentage of total comparison interactions.
It also calculates:
- Total comparison interactions.
- Most consulted criterion.
When an authenticated user is available, the user's UID can also be associated with the telemetry event.
This provides the processing required to transform the BQ3 telemetry into aggregated results.
The review flow includes access to the device camera so the user can take a photo of a dish when creating a restaurant review.
The user can:
- Take a photo using the device camera.
- Select a photo from the gallery.
- Preview the selected photo before publishing.
- Remove or replace the selected photo.
- Continue publishing the review without a photo if the selection is cancelled.
This functionality was integrated into the existing WriteReviewScreen without changing the general review flow.
The app uses the current waiting-time context of the selected restaurant to decide whether an alternative should be shown.
Recent student-reported waiting information is used when available. If there is no recent reported information, the restaurant's default waiting information is used as a fallback.
When the current waiting-time context is high (WaitTime.over15), the app makes an alternative restaurant available to the user.
If the waiting-time context is not high, the alternative is not shown.
This makes the recommendation dependent on the user's current restaurant context instead of displaying the same recommendation at all times.
The alternative restaurant is selected using multiple criteria instead of only one.
The recommendation considers:
- Waiting time: 40%
- Walking distance: 25%
- Rating: 20%
- Average menu price: 15%
The currently selected restaurant and closed restaurants are excluded from the alternatives.
The smart recommendation is implemented through SmartRestaurantStrategy, which implements the common RestaurantRecommendationStrategy interface.
RestaurantRepository uses the recommendation strategy instead of implementing the ranking algorithm directly.
This connects the smart feature with the Strategy design pattern while keeping the recommendation algorithm separated from the UI and repository flow.
BQ3 telemetry can be associated with the currently authenticated user.
The existing authentication flow provides access to the current user's UID. This value is injected into HomeViewModel through authenticatedUserId.
When a BQ3 interaction is recorded, HomeViewModel passes the current UID to TelemetryService, which can include it in the telemetry event.
No new authentication flow was created. The implementation uses the existing authentication system and integrates it with BQ3 analytics.
BQ3 uses Cloud Firestore as an external backend service.
Comparison interactions are sent through TelemetryService and stored as documents in the telemetry_events collection.
Each comparison event contains information such as:
- Event ID
- Event name
- Timestamp
- Platform
- Comparison criterion
- Restaurant ID
- Authenticated user ID when available
The collection flow is:
User interaction → View / HomeViewModel → TelemetryService → Cloud Firestore
The processing flow is:
Cloud Firestore → BQ3AnalyticsService → Aggregation → BQ3 result
Catalina implemented the collection and processing flow for BQ3.
TelemetryService is responsible for recording the comparison interactions in Firestore.
BQ3AnalyticsService is responsible for reading and aggregating the comparison events.
The analytics flow is:
Interaction → TelemetryService → telemetry_events → BQ3AnalyticsService → counts and percentages → BQ3 result
The analytics service recognizes:
pricedistancewaiting_timemenudietary_preferences
It calculates the interaction count and percentage for each criterion and identifies the most consulted criterion.
This keeps telemetry collection and analytics processing as separate responsibilities.
The application follows an MVVM-based separation between the UI, application state and data/services.
Catalina's implementation contributes to this architecture through the restaurant comparison and recommendation flow:
HomeScreen → HomeViewModel → RestaurantRepository / TelemetryService
HomeViewModel manages:
- The currently selected restaurant.
- Current/reported waiting-time context.
- Whether a high-wait context exists.
- Access to the alternative restaurant recommendation.
- BQ3 telemetry interactions.
- The authenticated user ID used by BQ3 telemetry.
The ViewModel delegates restaurant access and recommendations to RestaurantRepository and analytics event recording to TelemetryService.
BQ3 result processing is kept separately in BQ3AnalyticsService.
This avoids placing recommendation, state-management, Firestore telemetry and analytics-processing logic directly inside the UI.
Catalina implemented the Strategy behavioral design pattern for restaurant recommendations.
The common interface is:
RestaurantRecommendationStrategy
The available implementations are:
FasterRestaurantStrategySmartRestaurantStrategy
RestaurantRepository depends on the strategy interface instead of containing a fixed recommendation algorithm.
The default implementation is SmartRestaurantStrategy, which ranks alternatives using waiting time, distance, rating and average menu price.
This allows the recommendation algorithm to be changed without modifying the UI flow or the repository's public recommendation method.
Catalina contributed to WriteReviewScreen by integrating the dish-photo flow into the existing review view.
The view supports camera/gallery selection, photo preview, removal or replacement of the selected image, cancellation and publishing a review without requiring a photo.
This contribution preserves the existing review functionality while adding the sensor interaction required for Sprint 2.
| Item | Feature |
|---|---|
| a. Sensor | GPS shows the student's real position on the map and measures walking distance and time to each restaurant |
| b. Type 2 BQ | BQ6: time students spend setting filters before choosing a restaurant, with and without the filter suggestion |
| c. Context aware | "Near you": detects the campus building the student is in and highlights the restaurants reachable in 3 minutes |
| d. Smart | Suggests the filters the student usually applies, learned from their own past searches |
| e. Authentication | Preferred building saved in the student's account (users/{uid}) and a Profile that shows the signed-in student's real data |
| f. External service | Real campus map with OpenStreetMap tiles and restaurants read in real time from Cloud Firestore |
BQ6: How much time on average does a student spend setting up filters such as price, distance, and dietary preferences before finally selecting a restaurant profile?
A filter session starts with the first interaction on the Explore screen and ends when the student opens a restaurant from the results. The app sends a filter_session_completed event with the duration, number of filters, filter changes, searches, and whether the suggestion was used.
BQ6AnalyticsService calculates the average and median time to decide, the distribution by time range, and the comparison with and without the suggestion. This shows how much friction students face and whether the suggestion reduces it.
BQ9: How often do students use the allergen and dietary restriction filters compared to the other filters in the app, and which restrictions are selected the most?
Every tap on Apply Filters in the Explore screen sends one filter_applied event per active filter to telemetry_events. Events from the same search share an applyId.
BQ9AnalyticsService groups the events by filter type and by dietary restriction to obtain the share of dietary filters against the others, the share of searches that use a dietary filter, the most used filter type and the most selected restriction.
The app uses the phone's GPS through geolocator. Before reading the position it checks that location is enabled and asks for permission. Readings with more than 50 m of error and positions outside campus are ignored.
The position updates the student's marker on the map and the walking distance and time to every restaurant (straight-line distance with the haversine formula, multiplied by 1.3 for detours and divided by 80 m/min).
If the GPS cannot be used, the app falls back to the student's preferred building, and the student can also pick a building from the "Where are you?" sheet.
The map adapts to the student's location without being asked. Using the current position (GPS or chosen building), the app finds the closest campus building within 120 m of its entrance and the restaurants reachable in 3 minutes on foot.
A "Near you" banner shows, for example, "Near RGD · 5 restaurants under 3 min · closest: Burger Play - RGD". Restaurants farther away are dimmed. The banner updates as the student walks, and tapping it selects the closest restaurant.
The Explore screen suggests the filters the student usually applies.
FilterSuggestionService reads the student's own filter_applied events, groups them by search, keeps the latest 10 searches, and suggests every filter that appears in at least half of them (and in at least two). With fewer than two searches there is no suggestion yet.
The suggestion appears as "Suggested for you · Vegan · < 10 min · Based on your last 4 searches" and is applied with one tap. It is hidden when those filters are already selected.
The student's preferred building is stored in their account in users/{uid}, using the same preferredBuilding field and codes (ML, SD, RGD…) as the Kotlin app. It is changed from the Profile screen and written with merge, so no other field of the document is removed.
After signing in, the preferred building becomes the location used when the GPS is not available. When the student signs out it goes back to ML.
The Profile screen shows the signed-in student's name, initials, email, number of published reviews (from the reviews collection) and preferred building, instead of the previous mock data.
The map uses OpenStreetMap tiles through flutter_map, with the camera limited to the campus.
Restaurants are read from the Cloud Firestore restaurants collection in real time and mapped with Restaurant.fromFirestore (location, price, category, dietary options, payment methods and menu). Changes in Firestore, such as a new rating, reach the app without reloading.
Juan José built the campus map, the Explore screen and the Profile data flow as MVVM components: CampusMapViewModel, ExploreViewModel and ProfileViewModel hold the state, and CampusRestaurantsRepository and UserProfileRepository handle Firestore. Location logic lives in UserLocationTracker, shared by the map and Explore.
Juan José implemented the Strategy behavioral design pattern for the student's location.
The common interface is LocationStrategy, with the implementations GpsLocationStrategy and BuildingLocationStrategy.
UserLocationTracker is the context: it starts with the GPS strategy and switches to the building strategy when the GPS fails or the student picks a building. Consumers only receive coordinates, so a new way of locating the student can be added without changing the map or the distance calculation.
Juan José implemented the Explore screen: filters by category, walking time, budget, dietary restriction and payment method, a list of matching restaurants ordered by distance with an empty state, and the "Suggested for you" card. He also built the real campus map (markers, "Where are you?" sheet, location controls and "Near you" banner) and the Profile screen with the signed-in student's data and the preferred building selector.
| BQ | Type | Implemented by | Platform | Rationale |
|---|---|---|---|---|
| BQ4 - How often do students open dish photos when they inspect a restaurant menu? | Type 2 | Karin | Kotlin | Shows whether dish photos get attention. The result is the share of menu inspection events where a student opened a photo |
| BQ6 - How much time on average do students spend configuring price, distance, and dietary filters before selecting a restaurant? | Type 2 | Samuel | Kotlin | Analyzes decision friction in restaurant discovery based on campus building origin, budget, and dietary preferences. |
- BQ4 was changed to make it more specific and measurable. The new version focuses on how often students open dish photos during menu inspection, which can be directly measured through telemetry events.
When a student opens a dish photo, the app saves an event. The event goes through RestaurantViewModel and AnalyticsRepository, which adds a session ID. The app saves it in the background in Cloud Firestore (telemetry_events). A Python service reads the events from Firestore and keeps only the menu inspection events. Then it counts the events where a photo was opened and calculates the percentage: photo events divided by all menu inspection events. This percentage is the BQ4 result. The QR review events are also saved, but BQ4 does not use them.
| Event | When | Params |
|---|---|---|
karin_bq_menu_inspection |
The student opens a dish photo in a restaurant menu | "restaurant_id", "dish_id", "dish_count", "checked_photos", session_id
|
qr_review_submission |
The student scans the QR and sends or cancels the review | "restaurant_id", "rating", "is_completed", "session_id" |
When a student configures filters in ExploreScreen and clicks "Apply Filters", the app records the duration of the configuration session (in seconds) along with the selected origin building, budget range, and dietary restrictions (vegan, gluten-free). The event is dispatched via AnalyticsRepository with an active session ID and persisted to Cloud Firestore (telemetry_events). The Python analytics microservice (analytics_pipeline/app.py) queries Firestore, extracts all samuel_bq_filter_session events, computes the average duration in seconds, categorizes times into distribution buckets (<5s, 5-10s, 10-20s, >20s), and identifies the most consulted campus reference buildings (ML, RGD, Franco, SD, C, W).
| Event | When | Params |
|---|---|---|
samuel_bq_filter_session |
The student adjusts dining filters and clicks "Apply Filters" | "duration_sec", "building", "budget_range", "is_vegan", "is_gluten_free", session_id
|
Justification The application is organized into User Interface (UI), ViewModel, and repository layers, primarily following the MVVM pattern. Jetpack Compose screens do not access Firestore directly; instead, they use ViewModels that delegate access to the repositories. Firebase serves as the primary backend: authentication handles user management and password recovery, while Cloud Firestore stores data regarding restaurants, users, reviews, and telemetry events. The Python pipeline is decoupled from the Android application; it consumes events from "telemetry_events" and calculates BQ4/BQ6 metrics. This separation ensures that analytical processing does not depend on mobile presentation logic.
Justification: Communication in the app goes in one direction: the screen talks to the ViewModel, the ViewModel talks to the repository, and the repository talks to the external service (Firebase). This way the screens do not know anything about Firebase or how the data is saved. For example, ReviewScreen does not write to Firestore: it sends the action to ReviewViewModel, which uses RestaurantRepository and AnalyticsRepository. The state comes back the other way with StateFlow: the repository or ViewModel updates it and the Compose screen redraws by itself. Because of this, changes like a new restaurant rating or a login state show up in the UI without extra code.
| Category | Pattern | Where | Responsible | Why |
|---|---|---|---|---|
| Architectural | MVVM |
ReviewScreen, ReviewViewModel, RestaurantRepository
|
Karin | The screen and the logic are separate, so we can test the logic. |
| Data access | Repository |
PopularDishesRepository, RestaurantRepository.saveReview
|
Karin | The ViewModel does not know about Firestore |
| Creational | Dependency Injection | ReviewViewModel(analyticsRepository, restaurantRepository) |
Karin | We can use mocks in the unit tests. |
| Behavioral | Observer |
StateFlow + collectAsState in RestaurantDetailScreen
|
Karin | The screen updates when the data changes. |
| Structural | Facade | AnalyticsRepository.logEvent |
Karin | One call hides the local list, the Firestore write and the HTTP send. |
| Architectural | MVVM |
ExploreScreen, MapScreen, RestaurantViewModel
|
Samuel | The UI passively observes reactive ViewModels via StateFlow, separating GIS rendering and filter logic from data streams. |
| Data access | Repository |
RestaurantRepository, AnalyticsRepository
|
Samuel | Acts as Single Source of Truth, handling in-memory caching and real-time synchronization with Cloud Firestore. |
| Behavioral | Observer |
StateFlow + collectAsStateWithLifecycle in ExploreScreen & MapScreen
|
Samuel | The screen updates reactively when filters, distance thresholds, or reference buildings change without tight coupling. |
| Behavioral | Strategy | Dynamic filtering predicates & campus walking distance calculation in RestaurantRepository
|
Samuel | Interchangeable filtering strategies based on user criteria (origin building, max walking time, budget, dietary restrictions). |
| Structural | Adapter |
AndroidBridge interface in MapScreen
|
Samuel | Bridges external Leaflet JavaScript map click events with native Jetpack Compose callback methods. |
| Quality attribute | Tactic | Where | Responsible |
|---|---|---|---|
| Security | Authentication | Password recovery through Firebase Auth reset email | Karin |
| Security | Input validation | "validateEmail" is applied before "resetPassword" calls Firebase | Karin |
| Security | Limit exposure of information | Generic reset message, so account existence is not revealed | Karin |
| Security | Least privilege | Camera permission is requested at runtime only when the QR scanner opens | Karin |
| Performance | Asynchronous processing | AnalyticsRepository.logEvent sends events to Firestore in the background, so the screen never waits for the network | Karin |
| Performance | Caching | The BQ4 analytics reader caches the Firestore pages for 10 seconds | Karin |
| Maintainability | Separation of concerns | Telemetry collection ("AnalyticsRepository") is separated from BQ4 processing ("bq4_analysis.py") | Karin |
| Testability | Dependency injection | ViewModels receive repositories through the constructor, so JUnit tests use Mockito mocks | Karin |
| Performance | Caching |
RestaurantRepository maintains an in-memory cache of restaurants and menu items to prevent redundant Firestore network reads |
Samuel |
| Performance | Pre-computation | Pre-computed walking distance matrix per campus building (ML, C, RGD, Franco, SD, W) for instant client-side calculation | Samuel |
| Security | Data Minimization | Campus location is determined through user-selected origin building without harvesting background GPS traces | Samuel |
| Maintainability | Layered Separation | Cartographic Leaflet web engine is decoupled from native Compose navigation via strongly typed bridge interfaces | Samuel |
We chose a over c
| Item | Feature |
|---|---|
| a. Sensor | QR scanner with the real camera and a runtime camera permission request |
| b. Type 2 BQ | Telemetry for dish photo inspection and QR visits, and the analytics that calculates the percentage |
| d. Smart | "Popular with students" label: it recommends the most viewed dishes of a restaurant, using the real behavior of other users (telemetry events). |
| e. Authentication | Password recovery by Firebase reset email, from the login screen |
| f. External service | QR reviews saved in the backend (Cloud Firestore): rating, comment and user id go to the reviews collection, and the restaurant rating and review count are updated. |
| Item | Feature |
|---|---|
| c. Context-Aware | Campus-aware dining discovery: The app dynamically recalculates walking times, contextual distance badges ("X min walk from [Building]"), and reachable eateries based on the student's selected campus reference building (ML, C, RGD, Franco, SD, W). |
| b. Type 2 BQ | BQ6: Filter session duration tracking (samuel_bq_filter_session) measuring time spent configuring price, distance, and dietary criteria, processed by the Python Flask microservice to compute averages and distribution histograms. |
| d. Smart | Interactive Campus Map with spatial building routing and multi-criteria constraint solver: cross-references geographic coordinates of campus buildings and restaurants with budget, dietary, and walking-time constraints. |
| e. Authentication | Firebase Authentication: user registration, login, institutional email domain validation (@uniandes.edu.co), session persistence, and secure logout in ProfileScreen. |
| f. External service | Google Cloud Firestore & GIS Web Tile Services: bidirectional synchronization of restaurant catalog and telemetry events to Cloud Firestore, along with real-time satellite tile streaming from Esri and OpenStreetMap. |