Sprint 2 Deliverable - ISIS3510-S1-G15/Backend GitHub Wiki

Table of contents

Business Questions

Alejandro Franco

How often do users check or inquire about a restaurant’s occupancy level before deciding to go? - Type 2

To answer the Type 2 business question "How often do users check or inquire about a restaurant’s occupancy level before deciding to go?", I focused on how the application's interface prominently displays real-time occupancy indicators and crowd status tags directly on the restaurant cards across the Home, Map, and Saved screens. By analyzing user interaction patterns with these specific UI components where live capacity data (such as active customer counts and open/closed crowding badges) is shown, the system tracks how frequently students inspect crowd levels before choosing a spot. This directly answers the business question by quantifying user attention toward real-time occupancy metrics, helping evaluate whether these visible crowding indicators effectively guide student decision-making between classes without requiring deep code implementation.

Kevin Hernandez

Which filters (diet, budget, distance, available time) are most commonly used when searching for a place to eat? - Type 2

To answer the Type 2 business question "Which filters (diet, budget, distance, available time) are most commonly used when searching for a place to eat?", I focused on tracking user interactions with the filter chips available in the UI. The application features horizontal scrolling categories (like "Dining Hall", "Café", "Asian") on the Discover screen and quick-filter tags (like "# Vegan options", "# Halal", "# Open late") on the Search screen. By logging an analytics event every time a user taps one of these specific chips, the system captures exactly which criteria students prioritize when actively looking for food. This directly answers the business question by revealing whether users are more driven by dietary restrictions, convenience, or cuisine types, allowing the team to optimize the default sorting and prominently display the most in-demand filters.

Jeronimo Vasquez

How often do users go back and edit their saved dietary preferences after their first use? - Type 3

To answer the Type 3 business question "How often do users go back and edit their saved dietary preferences after their first use?", I implemented event tracking around the profile management flow. The app stores the user's initial onboarding survey results locally and displays them comprehensively in the My Profile screen, which includes detailed sections for "Dietary Restrictions" and "Cuisine Preferences". By registering an event whenever a user taps the "Edit" button and successfully saves changes to these specific fields after their initial setup, the system tracks the long-term relevance of the profile feature. This directly answers the business question by measuring if users actively maintain their personalization settings over time or if the profile screen becomes a forgotten feature, informing future decisions on whether to periodically prompt users to update their preferences.

Sofía Alcalá

What categories of food or restaurants do users search for without getting any results (gaps in the listed offerings)? - Type 3

To answer the Type 3 business question "What categories of food or restaurants do users search for without getting any results?", I implemented an analytics pipeline connecting the app to a real backend service. When a user submits a search on the Search screen (by pressing Enter) and no restaurants or key words match their query, the app sends the search term to a FastAPI endpoint (POST /analytics/zero-result-search), which stores it in a SQLite database along with a timestamp. A second endpoint (GET /analytics/search-gaps) aggregates this data, grouping and counting how many times each term has been searched without results, returning the terms ordered from most to least frequent. This directly answers the business question by surfacing concrete gaps in the app's current restaurant offerings. For example, if "mexican" or "breakfast" repeatedly appear with no matches, that signals demand the team could address by adding partner restaurants in those categories. The pipeline can also be exported as a CSV report (GET /analytics/search-gaps/export) for easier review outside the app.

An example of unsuccessful searches and what they would look like:

Camilo Tellez

What percentage of users complete the food preferences survey during onboarding? - Type 2

To answer the Type 2 business question "What percentage of users complete the food preferences survey during onboarding?", I instrumented the onboarding survey, which is the first screen of the Flutter app and has five steps. Every time the survey is opened for the first time, the app creates a session id and sends a "started" event to the FastAPI backend (POST /analytics/onboarding-event); after that it sends a "step" event each time the user moves forward and a "completed" event when the preferences are saved. Each event is stored in SQLite with its timestamp. A second endpoint (GET /analytics/onboarding-completion) counts how many sessions started and how many were completed, returns the completion rate as a percentage, and also shows how many sessions reached each step. Retaking the survey from the profile is not tracked, so the rate only reflects the first-time onboarding. This answers the question directly and goes one step further: if many users drop off at the same step, that step is the one making the onboarding too long or confusing, which tells the team exactly what to simplify to improve the experience. The summary can also be exported as CSV (GET /analytics/onboarding-completion/export).

Juan Felipe Ochoa

On average, how many times does a user open the app during a typical academic week? - Type 1

This question comes from the Type 1 (app telemetry) list of our Sprint 1 deliverable. It is Type 1 because it measures how the app itself is used, and its answer is for the team, not for the end user: it tells us whether students make the app part of their weekly routine on campus or open it only occasionally, which is the base to evaluate any future feature.

Answering it correctly requires knowing who opens the app. Counting opens per device would mix users who share a phone and split one student who uses two devices. For that reason this BQ is built on top of user authentication: only logged-in users reach the main app, and every open is attributed to the account in the session token.

The app records an open when a logged-in user enters the app (after logging in or when a saved session is restored) and every time they come back from the background after at least 30 seconds away, so quickly checking a notification is not counted twice. Each open is sent to the backend (POST /analytics/app-open, protected with the user's JWT) together with the meal moment of that time (Breakfast, Mid-morning snack, Lunch, Afternoon snack, Dinner or Late night), which is the same context the app uses on the Discover screen. GET /analytics/app-opens/weekly groups the opens by user and by calendar week and returns the average number of opens per user per week (the answer to the question), together with the number of active users and how the opens are distributed across meal moments. GET /analytics/app-opens/export produces a CSV report with one row per user and week, using only the internal user id (no names or emails).

Analytics Pipeline

Alejandro (occupancy checks, Type 2):

When a user views or interacts with the occupancy indicators and crowd status badges on the restaurant cards of the Home, Map, and Saved screens, the app registers an event identifying the restaurant and the moment it happened. The event is sent through an HTTP POST request to the shared FastAPI backend's /analytics endpoints and stored with a timestamp in the SQLite database. A GET endpoint then counts how often occupancy indicators are inspected per restaurant and compares that with the restaurants users end up selecting. The resulting rate answers the business question and shows whether the visible crowding indicators influence students' decisions.

Jeronimo (dietary preference edits, Type 3):

When a user taps "Edit" on the My Profile screen and successfully saves changes to the "Dietary Restrictions" or "Cuisine Preferences" fields after the initial setup, the app registers an edit event. The event travels through an HTTP POST request to the backend's /analytics endpoints and is stored with a timestamp in SQLite. A GET endpoint counts the edits made after first use, per user and over time. The result answers the business question by showing whether users keep their personalization up to date or the profile screen becomes a forgotten feature.

Kevin (most-used filters, Type 2):

Every tap on a filter chip, on the Discover screen's horizontal categories or the Search screen's quick-filter tags, registers an event that records which chip was tapped. The event is sent through an HTTP POST request to the backend's /analytics endpoints and stored with a timestamp in SQLite. A GET endpoint groups the stored taps by filter, counts how many times each was used, and orders them from most to least frequent. The ranking answers the business question by revealing whether students prioritize dietary restrictions, convenience, or cuisine type.

Sofia (Search-gap ranking, Type 3):

Regarding the business question "What categories of food or restaurants do users search for without getting any results (gaps in the listed offerings)?", it follows the analytics pipeline from start to finish. It begins at the capture stage: when a user searches for something in Flutter's search_screen and no restaurant matches, that moment is registered as the event to be measured. Right after that, in the ingestion stage, SearchAnalytics.logZeroResultSearch() sends that term through a POST request to the backend's /analytics/zero-result-search endpoint.
Once the backend receives the request, we move into storage: the searched term is saved as a new row in the zero_result_searches table in the SQLite database, along with a timestamp of when it happened. That raw data, accumulated over time, becomes useful information in the aggregation stage, where the GET /analytics/search-gaps endpoint groups all the stored searches, counts how many times each term repeats, and orders them from most to least frequent.Finally, in the consumption stage, that aggregated result is the direct answer to the business question: it tells us exactly which food categories students search for without finding anything, revealing real gaps in the offerings the app currently has listed. That result can be queried as JSON straight from the API or exported as a CSV file for review outside the app.

Camilo (Completion rate by step, Type 2):

The business question "What percentage of users complete the food preferences survey during onboarding?" follows the same stages. Capture: the survey_screen registers when the onboarding is opened, when the user reaches each step and when the preferences are saved. Ingestion: OnboardingAnalytics.logEvent() sends each event, together with the session id, through POST /analytics/onboarding-event. Storage: each event becomes a row in the onboarding_events table with a timestamp. Aggregation: GET /analytics/onboarding-completion counts distinct started and completed sessions, computes the completion rate and calculates the furthest step reached by each session. Consumption: the completion percentage answers the question, and the reach per step shows where users abandon the survey; the same result can be exported as CSV.

Juan Felipe (weekly app opens, Type 1):

When a logged-in user enters the app, either after logging in or when a saved session is restored, or returns from the background after at least 30 seconds away, the app registers an open event. The event is sent through POST /analytics/app-open, protected with the user's JWT, together with the meal moment of that time (Breakfast, Mid-morning snack, Lunch, Afternoon snack, Dinner, or Late night), and the backend attributes it to the account in the token, never to the device. Each open is stored with a timestamp in the SQLite database. GET /analytics/app-opens/weekly then groups the opens by user and calendar week and returns the average number of opens per user per week, the number of active users, and how the opens are distributed across meal moments. That average answers the business question by showing whether students make the app part of their weekly routine, and the same data can be exported as CSV through GET /analytics/app-opens/export, with one row per user and week and only the internal user id.

Architectural design

Kotlin

Overall system structure and components

Rationale: An Android application organized around a Jetpack Compose presentation layer, an activity-level coordination and state-management component, local data and persistence components, and a device-location component. The Compose interface includes the Discover, Search, Map, Saved, Profile, Detail, Survey, Write Review, and Reviews List screens, along with supporting components such as the crowding modal. Navigation, onboarding, shared UI state, and interactions with the persistence components are coordinated by MainActivity and CampusEatsMainScreen. Restaurant information is provided by a local, predefined catalog. Profile data, reviews, crowding reports, and saved restaurant IDs are persisted locally through ProfileStore and AppDataStore, using separate SharedPreferences files. Data models and recommendation logic are defined in the data package, while GPS and network-based location access are handled by a location component and shared with the interface. This separation keeps persistence and device-location access outside most screen components while preserving a lightweight architecture suited to the prototype.

How the main components interact and communicate.

Rationale: Centralized coordination through CampusEatsMainScreen, which maintains the active tab, overlay stack, selected and pending restaurants, profile, reviews, crowding reports, saved restaurant IDs, and shared location state. Screens receive the data they need and report user actions through callbacks such as onSelect, onSubmit, onBack, and onToggleSave. Restaurant selections from the Discover, Search, Map, and Saved screens are handled by the coordinator, which displays the crowding modal before opening the restaurant detail when applicable. Actions from the Detail, Profile, and review screens are also routed through the coordinator to display the review list, write-review form, or survey. Persistence requests are made by the coordinator to AppDataStore and ProfileStore, which use separate SharedPreferences files. This approach centralizes shared state and navigation instead of duplicating them across screens.

Restaurant Detail and Review Flow

Rationale: The restaurant flow uses CampusEatsMainScreen as the coordinator between screens and persistent data. When a restaurant is selected, the application first allows the user to report its crowding level and then opens its details. Reviews are created in WriteReviewView and passed back to MainActivity, which updates the application state and persists the reviews through AppDataStore. This keeps the screens focused on presentation and user interaction while persistence remains centralized.

Design Patterns

Category Pattern Where Responsible Why
Behavioral Observer (Compose observable state) Shared state is held in CampusEatsMainScreen using mutableStateOf, mutableStateListOf, and mutableStateMapOf. Compose observes these values and recomposes the screens that read them. MainActivity.kt Alejandro Franco It reflects changes—such as saving a restaurant or adding a review—in the UI without requiring manual view updates.
Behavioral / UI Unidirectional data flow and state hoisting The root coordinator passes data and callbacks to the screens; screens report actions such as selecting a restaurant or submitting a review. MainActivity.kt, HomeView.kt, WriteReviewView.kt Kevin Hernandez It centralizes state changes that affect multiple screens and lets UI components focus on rendering and collecting user input.
Structural Local store / persistence adapter AppDataStore saves and loads reviews, crowding reports, and saved restaurant IDs; ProfileStore handles the profile and onboarding status using SharedPreferences. AppDataStore.kt, ProfileStore.kt Jeronimo Vasquez It encapsulates preference access and serialization, so screens do not need to know storage keys or JSON formats. This is a local persistence abstraction; it is not a Repository with remote or interchangeable data sources.

Architectural Tactics

Quality attribute Tactic Where Responsible
Local availability Persist user data on the device so it can be loaded when the app is reopened. No server synchronization is implemented. Reviews, crowding reports, and saved IDs in AppDataStore; profile and survey completion in ProfileStore. AppDataStore.kt, ProfileStore.kt Jeronimo Vasquez
Performance Load images asynchronously through Coil Compose rather than implementing image downloads in each screen's rendering logic. Components using AsyncImage, including Home, Search, and Map. HomeView.kt, SearchView.kt, MapView.kt Alejandro Franco
Maintainability Separate presentation, data models, and persistence access into distinct components and files. ui and data packages; for example, Compose screens are separated from Restaurant, Review, AppDataStore, and ProfileStore. ui/, data/ Kevin Hernandez
Maintainability Centralize screen and overlay transitions, and define screens through data and callbacks. CampusEatsMainScreen manages tabs, restaurant details, onboarding, and review flows; screens receive callbacks for events. MainActivity.kt Kevin Hernandez
UI consistency Keep shared state at the root and pass it to the screens that need it. Saved IDs, crowding reports, and reviews are passed from CampusEatsMainScreen to the corresponding views. MainActivity.kt Alejandro Franco

Flutter

Architectural pattern

MVC is an architectural pattern that separates an application into three interconnected components: the Model, which manages data and business logic, the View, which defines what the user sees, and the Controller, which handles user input and coordinates between the Model and the View. Flutter's own widget structure naturally maps onto MVC, and our codebase follows that separation consistently. Taking detail_screen as a representative example:

  • The Model is represented by plain data classes such as Restaurant and MenuItem, which hold the restaurant's data and have no knowledge of how that data is displayed.
  • The View is the build() method of each widget, which is purely responsible for rendering the current state onto the screen, it contains no decision-making logic of its own.
  • The Controller is the State class behind each stateful screen (e.g. _DetailScreenState). It receives user input, such as tapping the bookmark icon or the "Report crowding level" button, and decides what should happen next: updating local state, calling OccupancySurvey.show(), or triggering _loadReports() to refresh the data shown by the View.

MVC keeps each piece of the screen responsible for a single concern, which made it possible for different team members to work on different screens in parallel without one person's view logic interfering with another's data logic. It also made our app easier to extend incrementally: new user interactions (like reporting occupancy) could be added by extending the Controller layer of a screen without needing to touch how the Model is defined or how the View renders the rest of the screen.

MVC in login_screen and home_screen

sequenceDiagram
    actor U as User
    participant V as LoginScreen build() (View)
    participant C as _LoginScreenState (Controller)
    participant S as AuthService (service)
    participant M as AuthUser / userProfile (Model)
    participant G as AuthGate
    U->>V: types email + password, taps "Log in"
    V->>C: _submit()
    C->>C: _validate() (uniandes email, 8+ characters)
    alt invalid data
        C->>C: setState(_error = message)
        C-->>V: rebuild shows the error
    else valid data
        C->>C: setState(_loading = true)
        C->>S: login(email, password)
        S-->>C: AuthUser (or AuthException)
        C->>M: userProfile.name = user.name
        C->>G: enterApp(user, isNewUser)
        G-->>U: survey (new account) or Discover
    end

The diagram shows how the login follows MVC. The View (build()) only draws the fields, the error text and the button, and calls the Controller when the user taps it. The Controller (_LoginScreenState) owns the state of the screen (loading, error, login or register mode), validates the fields before calling the network, asks AuthService to authenticate and decides where to navigate. The Model is AuthUser (who is logged in) and userProfile, which Discover and Profile listen to. The Discover screen follows the same split: _HomeScreenState (Controller) reads the current time every minute and asks lib/utils/meal_context.dart for the meal moment and the open-now pick, so the rules live outside the View. Keeping validation and navigation in the Controller let us change the screen's design without touching the authentication logic.

Design patterns and architectural tactics

Architectural tactic: Encapsulate (Modifiability) - Sofia Alcalá

Encapsulate is a modifiability tactic that reduces the cost of change by hiding a part of the system likely to change behind a stable interface, so that when that part changes, the modification stays contained instead of rippling through the rest of the codebase.

The data source behind Occupancy_service was deliberately treated as the "volatile" part of the system. Right now, submitReport() and getReports() persist data locally on the device using shared_preferences. Migrating this feature to the backend, so reports are shared across all users instead of staying on a single device, is planned as a near-term next step, and the service was intentionally designed so that migration only requires changing the implementation inside occupancy_service, without needing to touch detail_screen.dart, occupancy_survey.dart, or crowding_badge.dart.

Because the UI depends only on the stable interface exposed by occupancy_service, that means none of the method names, parameters, and return types reference shared_preferences or any storage detail directly. This means that when the migration to the backend happens, it should require touching exactly one file, which is the core promise of the Encapsulate tactic: isolating a likely source of change so the rest of the system remains stable even before that change is made.

Design Pattern: Service Layer/Facade - Sofia Alcalá

A Service Layer pattern introduces an intermediate class that sits between the UI and the actual data source, exposing a small, stable set of methods while hiding the implementation details of where or how the data is actually stored or retrieved.

occupancy_service (and similarly search_analytics) acts as this service layer for the crowding indicator feature. The UI components (detail_screen, occupancy_survey, and crowding_badge) never talk directly to a storage mechanism, they only call occupancy_service.submitReport(restaurantId, level) and occupancy_service.getReports(restaurantId). The service decides internally how those operations are actually carried out.

This pattern decouples the UI from the data layer, which is especially valuable where the underlying data source was expected to change as the app matured (starting local, moving to a backend). By giving the UI a single, simple contract to depend on, any team member could build and test screens against occupancy_service without needing the backend to exist yet, and without needing to rewrite UI code once it did.

Architectural contribution: MVC in survey_screen - Camilo Tellez

sequenceDiagram
    actor U as User
    participant V as SurveyScreen build() (View)
    participant C as _SurveyScreenState (Controller)
    participant M as userProfile (Model)
    participant G as OnboardingAnalytics (Gateway)
    participant B as FastAPI backend
    C--)G: initState: logEvent(session, 'started', 1)
    U->>V: taps "Continue"
    V->>C: _next()
    C->>C: setState(_step++)
    C--)G: logEvent(session, 'step', n)
    U->>V: taps "Save Preferences"
    V->>C: _next() on last step
    C->>M: userProfile.value = answers
    C--)G: logEvent(session, 'completed', 5)
    G--)B: POST /analytics/onboarding-event

The diagram shows how the survey follows MVC. The View (build()) only draws the current step and the buttons. The Controller (_SurveyScreenState) owns the step counter, decides what "Continue" means (next step or save), writes the answers into the Model (userProfile, a ValueNotifier that Home and Profile listen to) and reports each event to the analytics gateway. The analytics calls (dashed arrows) are not awaited, so the survey moves to the next step immediately even if the backend is slow or offline. Keeping this logic in the Controller allowed the tracking to be added without changing how the steps are rendered.

Design Pattern: Gateway - Camilo Tellez

OnboardingAnalytics is a Gateway: a single class that wraps the access to an external system, the analytics backend. The survey only calls OnboardingAnalytics.logEvent(sessionId, event, step) and never builds URLs, JSON bodies or HTTP requests itself; the class also generates the session id that links the events of one run of the survey. If the endpoint, the base URL or the way events are sent changes, only this class has to be modified, and the screen stays the same.

Architectural Tactic: Fire-and-forget asynchronous logging (Availability / Performance) - Camilo Tellez

Analytics events are sent asynchronously and any error is caught inside the gateway. A failed or slow request never blocks the onboarding or crashes the app, so new users can always finish the survey even without a connection; in that case only the analytics event is lost, not the user's preferences.

Design Pattern: Singleton - Juan Felipe Ochoa

The Singleton pattern guarantees that a class has only one instance and gives the whole application a single global point of access to it.

ApiClient (lib/services/api_client.dart) is implemented as a Singleton: its constructor is private (ApiClient._internal()), the only instance is created once in a static field, and the factory constructor ApiClient() always returns that same instance. It is the single place that knows the backend's base URL, the request timeout (5 seconds), how to send and receive JSON and, after login, the user's session token, which it adds to every request.

Justification: the session token is the main reason for this pattern. AuthService sets the token once after login, and from that moment AppUsageAnalytics (and any service that uses ApiClient) sends it automatically because they all use the same instance; on logout it is cleared for all of them at once. With separate instances, each service would need its own copy of the token and they could get out of sync, for example one service still sending the token of a user who already logged out. The pattern also keeps the backend URL in one place, so moving from the emulator to a physical phone or a deployed server means changing one line.

Architectural tactic: Authenticate actors (security) - Juan Felipe Ochoa

Protected endpoints (GET /auth/me and POST /analytics/app-open) use a FastAPI dependency, get_current_user, that rejects any request without a valid, non-expired JWT with 401. The user is always taken from the token, never from the request body, so one user cannot record data in another user's name. Registration only accepts institutional emails (@uniandes.edu.co), because the app is for the university community, and passwords need at least 8 characters and are stored only as bcrypt hashes.

sequenceDiagram
    actor U as Student
    participant L as LoginScreen
    participant AS as AuthService
    participant API as ApiClient (Singleton)
    participant BE as FastAPI /auth
    participant DB as SQLite users
    U->>L: email + password
    L->>AS: login(email, password)
    AS->>API: postJson("/auth/login")
    API->>BE: POST /auth/login
    BE->>DB: find user by email
    BE->>BE: bcrypt.checkpw(password, stored hash)
    BE-->>API: 200 {access_token: JWT valid 7 days, user}
    API-->>AS: token + user
    AS->>AS: save session in SharedPreferences, ApiClient.setToken(token)
    AS-->>L: AuthUser
    Note over API,BE: Every later request carries "Authorization: Bearer JWT"
    API->>BE: POST /analytics/app-open + Bearer JWT
    BE->>BE: get_current_user validates the JWT (401 if missing, invalid or expired)
    BE->>DB: insert app_open with user_id taken from the token

Justification of the diagram: it shows the two moments of the tactic. At login, the password is checked against its bcrypt hash and the backend answers with a signed JWT instead of keeping a session in memory. Afterwards, every protected request is authenticated by validating that JWT, and the user id is read from the token, never from the request body, so a request cannot record data in another user's name. The same message is returned for an unknown email and a wrong password, so the login does not reveal which emails are registered.

Architectural tactic: Graceful degradation without the backend (availability) - Juan Felipe Ochoa

If the server cannot be reached when the app starts, AuthService keeps the saved session instead of logging the user out, so the student can keep browsing restaurants offline. App-open events fail silently, and the context-aware Discover screen works entirely on the phone (device clock and opening hours), so it never depends on the network. The only effect of a backend failure is that some analytics events are not recorded.

Implemented Features

Kotlin

Functionality that answers type 2 business questions - Alejandro Franco

To fulfill the requirement for a feature that addresses direct user experience and decision-making, the application implements real-time occupancy indicators and crowding status badges explicitly integrated into the restaurant cards across the Home, Map, and Saved screens. Rather than requiring complex code structures, this feature makes crowding data instantly visible to students right when they are browsing options between classes. By displaying active crowd counts and open/closed status tags directly next to each restaurant name, the interface allows users to check occupancy levels seamlessly before deciding where to go, providing the core visual touchpoint through which user engagement with live capacity data can be observed and evaluated.

Functionality that uses a sensor (GPS) - Jeronimo Vasquez

To fulfill the requirement of using a device sensor, the Map View implements GPS-based location tracking to calculate distances between the student and the restaurants. This feature accesses the phone's location services to determine the user's current coordinates. Once the location is obtained, the app computes the real-time distance to each pinned restaurant on the OpenStreetMap interface. This directly enhances the user experience by allowing students to see exactly how far they are from food spots on campus, answering the practical need of minimizing walking time between classes.

Context-aware functionality - Kevin Hernandez

To fulfill the context-aware requirement, the Search View implements an adaptive filtering mechanism based on the restaurants' real-time operational status. The app uses the current time to evaluate the opening hours of each location, determining whether a spot is currently "Open" or "Closed." This context is then used to enable dynamic filtering options, allowing students to filter search results to display only places that are "Open Now". This functionality adapts seamlessly to the time of day, ensuring that users only receive actionable recommendations relevant to their immediate context.

Functionality that uses external services - Kevin Hernandez

To satisfy the external services requirement, the Map View integrates OpenStreetMap (OSM) as a third-party mapping service. Instead of relying on static images, the application fetches and renders map tiles dynamically from OSM servers to display the exact coordinates and layout of the Universidad de los Andes campus and its restaurants. This implementation provides an interactive map interface without the need for paid API keys (like Google Maps), effectively leveraging an external data source to provide crucial spatial context for users navigating the campus.

Smart feature - Jeronimo Vasquez

As a smart feature, the application implements dynamic recommendations and wait-time estimations on the Home and Map screens. By combining the calculated distance between the user (via GPS) and the restaurants with operational statuses, the app dynamically updates the estimated wait times (e.g., "2-5 min wait" or "10-15 min wait") displayed on the cards. Rather than static text, these estimations are smartly presented to guide user decisions when time is constrained, fulfilling the need to intelligently aggregate contextual data (location and time) into a single actionable metric for the student.

Interactive Campus Map & Location Status - Alejandro Franco

To further enhance spatial awareness and location-based decision-making, the application implements an interactive Campus Map screen backed by OpenStreetMap. This feature dynamically renders the university campus area and overlays pins for nearby restaurants, embedding real-time operational status (such as "Open" or "Closed") directly into the map markers and descriptive info-windows. By leveraging the device emulator's location configuration tools, the app accurately tracks and resolves proximity based on the specified coordinates. This visual integration allows students to instantly inspect nearby food options, review operating states at a glance, and bridge the gap between geographic positioning and immediate dining choices.

Flutter

Functionality that uses external services - Sofia

The zero-result search tracking functionality mentioned before at the BQ part satisfies this requirement because it connects the Flutter app to an external service, a FastAPI backend running independently from the app itself, communicating over HTTP. When a user's search yields no results, the app sends an HTTP POST request to the /analytics/zero-result-search endpoint, which persists the data in a SQLite database hosted by the backend rather than storing it locally on the device. This is distinct from the authentication requirement, since it doesn't involve logging in a user, it's a data-driven feature that depends entirely on a live network connection to a server the team built and controls, fulfilling the requirement that at least one non-authentication feature be connected to the backend.

Functionality that uses external services - Camilo Tellez

Two of my features depend on services outside the app. First, the onboarding survey tracking (my Type 2 BQ) sends every "started", "step" and "completed" event over HTTP to the team's FastAPI backend (POST /analytics/onboarding-event), which stores them in its SQLite database and computes the completion rate in GET /analytics/onboarding-completion; this is a non-authentication feature connected to the backend, and the data is kept on the server rather than on the device, so it aggregates the sessions of all users. Second, the Map screen loads its map tiles from the OpenStreetMap tile server through flutter_map, so the real streets around campus are downloaded on demand instead of being bundled in the app. In both cases the app keeps working if the service is not reachable: analytics errors are caught inside the gateway, and the rest of the Map screen (distances, list and recommendation) does not depend on the tiles.

Smart feature - Sofia

As mentioned before during the Sprint 1, to help students avoid arriving at a full restaurant, the app implements a crowdsourced crowding indicator built on the same real-time occupancy question raised during the empathy interviews. From the restaurant's detail screen, any user can report how busy a place currently is by selecting one of three options (Not crowded, Moderate or Busy) through a simple survey dialog. At this point, these reports are stored locally on the device using Flutter's local storage (SharedPreferences), tied to each specific restaurant's ID.

What makes this a smart feature is that the app doesn't just display raw reports, it aggregates and interprets them into a single, actionable conclusion. The app calculates the average of the stored reports for a given restaurant and classifies that average into a clear label and color (green "Not crowded," orange "Moderate," or gray "Busy"), rather than forcing the user to interpret individual data points themselves. This same aggregated value also drives a five-bar visual indicator, giving students an immediate sense of how full a place is at a glance.

The feature is not yet connected to the backend, meaning reports are currently only visible on the device that submitted them rather than shared across all app users.

Functionality that answers type 2 business questions - Camilo Tellez

The onboarding survey completion tracking answers the Type 2 business question described in the BQ section. While a new user goes through the food preferences survey, the app reports when it was started, each step reached and when it was completed, and the backend turns these events into a completion rate and a drop-off per step. This directly improves the user experience: it shows whether the onboarding is too long and which question makes users quit, so the team can shorten or simplify that step.

Functionality that uses a sensor (GPS) - Camilo Tellez

The Map screen uses the phone's GPS through the geolocator package. When the screen opens, LocationService checks that the location service is turned on and asks for the location permission the first time (Android ACCESS_FINE_LOCATION / iOS "when in use"). After that, the screen subscribes to a position stream that emits a new position every time the student moves 10 m or more, so the blue dot on the map, the distances and the order of the list update by themselves while they walk around campus. The map is a real map (flutter_map with OpenStreetMap tiles, which does not need an API key) where every restaurant is pinned at its real coordinates. The stream is cancelled when the user leaves the screen to save battery, and if the GPS is off or the permission is denied, the screen keeps working and shows a message with a "Retry" option instead of failing.

Context-aware functionality - Camilo Tellez

The Map screen adapts its recommendation to the student's current context, combining three pieces of information: where they are (GPS), what time it is, and which places are open. The list is sorted from closest to farthest and each spot shows the distance and the estimated minutes walking (80 m per minute). A banner above the list changes according to the context: while the location is loading it says so; if it cannot be read it explains why and offers "Retry"; if the student is more than 600 m away from the campus center it tells them how far they are; and when they are on campus it recommends the closest open spot that fits the current meal (breakfast 6-11, lunch 11-15, snack 15-18, dinner 18-22), with a "Show" button that moves the map to it. For example, at 12:30 the closest place is Starbucks (40 m), but since it is lunch time the app recommends Yamato Sushi Wok (188 m), which actually serves lunch. This answers a need found in the interviews: students have little time between classes and want to know which open place that fits what they want to eat is the fastest to reach from where they are.

Functionality that allows user authentication - Juan Felipe Ochoa

The app now starts with a login screen. A new student creates an account with their name, institutional email (@uniandes.edu.co) and a password of at least 8 characters; the app validates the fields on the phone first and then calls the backend (POST /auth/register), which stores the password only as a bcrypt hash and returns a signed JWT. A new account continues to the food preferences survey, pre-filled with the account name, and then to the main app; an existing user logs in (POST /auth/login) and goes directly to the main app.

The session is kept on the phone, so the student does not have to log in every time: when the app starts, it validates the saved token with GET /auth/me and goes straight to Discover if it is still valid (tokens last 7 days). From the Profile screen the user can see which account is logged in and log out, which deletes the saved session, clears the profile data shown on screen and returns to the login screen. Errors are shown in plain language (wrong credentials, email already registered, no connection). Authentication is also what makes the Type 1 BQ possible: app opens are attributed to the logged-in account through its token.

Known limitation: the token is stored with SharedPreferences, which is not encrypted. Moving it to encrypted storage is planned for the next sprint.

Left to right: login screen, validation of the institutional email when creating an account, and the session (logged-in account and "Log out") in the Profile screen.

Functionality that is context aware - Juan Felipe Ochoa

The Discover screen adapts to the time of day without the user doing anything. The app reads the phone's clock and determines the current meal moment, using the same six moments the user chooses in the survey (Breakfast, Mid-morning snack, Lunch, Afternoon snack, Dinner, Late night). With that context:

  • The greeting changes ("Good morning / Good afternoon / Good evening, name").
  • "Today's Pick" is no longer a fixed restaurant: the app reads each restaurant's opening hours, keeps only the places that are open right now and recommends the best rated one, with an "OPEN NOW" label and a header such as "Lunch time · open now". If everything is closed, it says so instead of recommending a closed place.
  • If the user said in the survey that they usually eat at this moment, Discover adds "You usually eat at this time".
  • The screen refreshes every minute, so the pick and the labels change when a place opens or closes or when the meal moment changes, even if the app stays open.

Same Discover screen at 12:30 pm (lunch: greeting, open-now pick and the user's usual meal time) and at 11:00 pm (everything closed). The user did nothing different: only the time changed.

Ethics Video

Ethics Video On Youtube

Release

The release containing the Kotlin implementation for Sprint 2 can be found here:

Kotlin Release v2.0.0 — Sprint 2

The release containing the Flutter implementation for Sprint 2 can be found here:

Flutter Release v2.0.0 — Sprint 2