S4: Final Deliverable - ISIS3510-202510-T14/Documentation GitHub Wiki
- 1. Value Proposition
- 2. Micro-optimisations Strategies
- 3. Functionalities and design details
- 4. Ethics Video
- 5. Contributions
Implemented capability | How it works in the app | Tangible benefit for the student |
---|---|---|
Centralised calendar + geofenced push alerts | The mobile app keeps a single, always-up-to-date calendar for every varsity sport. A lightweight background service compares the phone’s GPS stream with venue geofences every few minutes. When two conditions are true—(a) a game is currently live or about to start, and (b) the user crosses the geofence radius—an instant-notification card appears with game details, a route link, and a “Quick Bet” shortcut. |
• Zero planning overhead: students learn about games when they are physically able to attend. • Eliminates FOMO by bridging the “information gap” seconds before kick-off. • Encourages spontaneous attendance, boosting real-world school spirit. |
Risk-free virtual betting with one-tap “Quick Bet” | The same proximity trigger automatically loads a modal bet-sheet pre-filled with the current matchup and default odds. The user can confirm or cancel with a single tap; the wager is stored locally and synced once data is available, ensuring it works even on congested stadium networks. | • Turns passive spectating into an interactive mini-game without real money. • One-tap flow removes friction and increases adoption among casual fans. • Creates a shared conversation starter in the stands (“What did you pick?”). |
Bet history view & smart suggestions | Every wager record is time-stamped and tagged with team, sport, and outcome once the score API resolves. A lightweight ML rule counts repeated picks per team and surfaces a “You might like…” recommendation banner before that team’s next fixture. | • Students feel the app “knows their tastes,” boosting retention. • Helps new bettors build confidence by highlighting patterns (“You’ve backed the Tigers 3 times—here’s their next game”). • Drives deeper engagement without pushy upselling. |
In-app merchandise marketplace | After each game, a server task refreshes the product catalog with score-specific or limited-edition items (e.g., rivalry-game shirts). Views, add-to-cart events, and purchases are counted per brand and category and fed to a dashboard. | • Immediate outlet to celebrate a win or soften a loss with official gear. • No need to open a browser—checkout happens natively, so impulse buys are faster. • Students can discover hot items through “Trending after tonight’s game” banners driven by real view data. |
-
Attendance backed by evidence
- Every geofence trigger is timestamped, so staff can see raw counts of students who were physically “at” or “near” a venue before and after we launched alerts.
-
Targeted promotion of under-attended sports
- A dashboard ranks sports by total bets. Disciplines with the lowest counts directly answer our Type 3 question “Which event series gather the least attention?”—giving marketers a clear list of teams to boost.
-
Operational peace-of-mind on game day
- Weekly boards for API error frequency and endpoint latency (Type 1 questions) let dev-ops fix bottlenecks before traffic spikes, protecting the fan experience.
Implemented metric | Sponsor-facing insight | Practical use |
---|---|---|
Product views by brand / category | See exactly which jerseys or caps get the most eyeballs. | Decide which SKUs to restock or discount next week. |
Proximity counts per game | Proof of how many students were inside marketing range. | Justify venue-specific ad buys or pop-up booths. |
Stream | Key data that makes it work | Why it scales |
---|---|---|
In-app sponsorships & event promos | Proximity counts + product-view logs → precise reach numbers in the sales deck. | Brands pay more when they can verify real exposure. |
Marketplace commissions | View-to-purchase funnel in the dashboard. | We optimise catalog order and pricing with the same data, raising take-rate. |
- Capture – GPS pings, bet logs, product views, API telemetry.
- Insight – Dashboards answer Type 1 (reliability), Type 2 (contextual triggers), and Type 3 (engagement gaps).
-
Action –
- Send timely alerts → more bets → more marketplace browsing.
- Surface low-engagement sports → targeted promos → broader fan base.
- Fix slow endpoints → smoother UX → fewer abandoned actions.
The same data that powers user delight is repackaged as attribution proof for sponsors and as optimisation fuel for our own monetisation flows.
Campus Picks moves students from passive scrolling to active cheering by combining real-time proximity alerts, frictionless virtual bets, and an on-the-spot merch store. The university receives concrete attendance and engagement numbers, sponsors get audit-ready exposure stats, and we generate sustainable income through ads, gear sales, and a planned premium layer—all validated by the metrics already live in our dashboards.
# | Local change | Why it qualifies as a micro-optimisation |
---|---|---|
1 |
Single-pass status bucketing – replace three separate .where() calls with one loop |
We keep the overall complexity O(n) and touch only a few lines; we simply avoid two extra passes over the same list and three temporary allocations. |
2 | Cached AndroidNotificationDetails instances |
Creating these objects and marshalling them through the platform channel is expensive. By turning them into static const singletons we allocate them once at class-load time and reuse forever. No API, logic, or architecture changes. |
3 |
itemExtent + cacheExtent in ListView.builder
|
The visual design is unchanged; we just give the Flutter engine a fixed height hint and pre-fetch off-screen items. This trims layout time and prevents “jank” while scrolling. |
4 |
Batch insertion for Recommended Bet persistence: replaced N individual SQLite INSERT calls with a single batch transaction via insertRecommendedBetsBatch(...) . |
Eliminates per-row journaling and multiple round-trips, reducing write time from O(N) separate operations to one, cutting database I/O by ~N×. |
5 |
Constant-time favourite look-ups: convert the favIds list into a Set once (favSet = favIds.toSet() ) and use favSet.contains(id) instead of List.contains . |
Changes membership test from O(F) per check to O(1), transforming overall loop complexity from O(M·F) to O(M+F), significantly speeding up the loop. |
- liveMatches = matches.where((m) => m.status.toLowerCase() == 'live').toList();
- upcomingMatches = matches.where((m) => m.status.toLowerCase() == 'upcoming').toList();
- finishedMatches = matches.where((m) => m.status.toLowerCase() == 'finished').toList();
+liveMatches.clear();
+upcomingMatches.clear();
+finishedMatches.clear();
+
+for (final m in matches) {
+ switch (m.status.toLowerCase()) {
+ case 'live': liveMatches.add(m); break;
+ case 'upcoming': upcomingMatches.add(m); break;
+ case 'finished': finishedMatches.add(m); break;
+ }
+}
Single-pass bucketing is a micro-optimisation because it tweaks just a few statements without touching design or complexity:
- Scope – limited to one assignment block; no new classes or APIs.
- Complexity – stays O(n); logic still traverses the list once, only redundant passes are removed.
- Benefit – two extra list scans and three temporary lists are eliminated, cutting CPU time and allocations during every fetch.
Screenshots Before Micro-optimisations:
Screenshots After Micro-optimisations:
Metric (Flutter DevTools) | Before (screens 1 & 2) | After (screens 3 & 4) | Δ / Improvement |
---|---|---|---|
fetchMatches – total CPU time with no matches notifications (call tree) |
901 ms | 547 ms | −39 % |
fetchMatches – self time |
256 ms | 102 ms | −60 % |
Avg UI frame time when specifically when changing to the matches view | 11-12 ms | 6-7 ms | ≈ −40 % |
Slow/jank frames (red bars over 16 ms) | several spikes | single minor spike | visibly smoother |
+ static const AndroidNotificationDetails _basicAndroid =
+ AndroidNotificationDetails(
+ 'basic_channel_id',
+ 'Basic Notifications',
+ channelDescription: 'Notifications for nearby events',
+ importance: Importance.max,
+ priority: Priority.high,
+ );
+
+ static const AndroidNotificationDetails _liveAndroid =
+ AndroidNotificationDetails(
+ 'live_channel',
+ 'Live Events',
+ channelDescription: 'Notifications for live events near you',
+ importance: Importance.max,
+ priority: Priority.high,
+ );
+
+ static const AndroidNotificationDetails _liveBetAndroid =
+ AndroidNotificationDetails(
+ 'live_channel',
+ 'Live Events',
+ channelDescription: 'Notifications for live events near you',
+ importance: Importance.max,
+ priority: Priority.high,
+ actions: [
+ AndroidNotificationAction(
+ 'bet_now_action',
+ 'Bet Now',
+ showsUserInterface: true,
+ ),
+ ],
+ );
+
+ static const NotificationDetails _basicDetails =
+ NotificationDetails(android: _basicAndroid);
+ static const NotificationDetails _liveDetails =
+ NotificationDetails(android: _liveAndroid);
+ static const NotificationDetails _liveBetDetails =
+ NotificationDetails(android: _liveBetAndroid);
-Future<void> _showNotification({
- required String title,
- required String body,
-}) async {
- const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
- 'basic_channel_id',
- 'Basic Notifications',
- channelDescription: 'Notifications for nearby events',
- importance: Importance.max,
- priority: Priority.high,
- );
- const NotificationDetails details = NotificationDetails(android: androidDetails);
-
- await flutterLocalNotificationsPlugin.show(0, title, body, details);
-}
+Future<void> _showNotification({
+ required String title,
+ required String body,
+}) async =>
+ flutterLocalNotificationsPlugin.show(0, title, body, _basicDetails);
-Future<void> _showLiveMatchNotification({
- required MatchModel match,
- required double distanceInKm,
- required bool withBetNow,
-}) async {
- AndroidNotificationDetails androidDetails;
- if (withBetNow) {
- androidDetails = const AndroidNotificationDetails(
- 'live_channel',
- 'Live Events',
- channelDescription: 'Notifications for live events near you',
- importance: Importance.max,
- priority: Priority.high,
- actions: <AndroidNotificationAction>[
- AndroidNotificationAction('bet_now_action', 'Bet Now', showsUserInterface: true),
- ],
- );
- } else {
- androidDetails = const AndroidNotificationDetails(
- 'live_channel',
- 'Live Events',
- channelDescription: 'Notifications for live events near you',
- importance: Importance.max,
- priority: Priority.high,
- );
- }
- NotificationDetails details = NotificationDetails(android: androidDetails);
-
- ...
-
- await flutterLocalNotificationsPlugin.show(
- withBetNow ? 1 : 2,
- withBetNow ? 'Live Event - Bet Now!' : 'Live Event Nearby',
- '${match.homeTeam} vs ${match.awayTeam}\nDistance: ${distanceInKm.toStringAsFixed(2)} km \n coords: ${match.location.lat}, ${match.location.lng} \n location: La caneca',
- details,
- payload: jsonEncode(payloadMap),
- );
-}
+Future<void> _showLiveMatchNotification({
+ required MatchModel match,
+ required double distanceInKm,
+ required bool withBetNow,
+}) async {
+ final NotificationDetails details =
+ withBetNow ? _liveBetDetails : _liveDetails;
+
+ final payloadMap = {
+ 'match': match.toJson(),
+ 'userUID': authService.value.currentUser?.uid,
+ };
+
+ await flutterLocalNotificationsPlugin.show(
+ withBetNow ? 1 : 2,
+ withBetNow ? 'Live Event - Bet Now!' : 'Live Event Nearby',
+ '${match.homeTeam} vs ${match.awayTeam}\nDistance: '
+ '${distanceInKm.toStringAsFixed(2)} km',
+ details,
+ payload: jsonEncode(payloadMap),
+ );
+}
Rationale: Cached AndroidNotificationDetails
as a micro-optimisation
-
checkProximityAndNotify()
iterates over every match and invokes the notification helper once per item. - In the original code each call allocated a fresh
AndroidNotificationDetails
and wrapped it in a newNotificationDetails
; those objects are large and must be marshalled across the platform channel. - Replacing the per-call construction with class-level
static const
singletons means a single allocation per notification type at app start-up, then mere reuse inside the loop.
This confined, statement-level change that removes redundant object creation—without altering design or architecture—fits the formal definition of a micro-optimisation.
Screenshots Before Micro-optimisations:
Screenshots After Micro-optimisations:
Profiling target | Before (ms) | After (ms) | Improvement |
---|---|---|---|
checkProximityAndNotify – total time |
350.28 | 115.55 | -67 % |
Allocations (malloc rows) |
42.8 | 23.1 | -46 % |
What the numbers mean
- With per-call object creation removed,
checkProximityAndNotify
itself runs ~3× faster. - Allocation time nearly halves, showing lower GC/heap pressure.
-return ListView.builder(
- itemCount: liveMatches.length,
- itemBuilder: (context, index) {
- final match = liveMatches[index];
- return MatchCardFactory.createMatchCard(match);
- },
-);
+return ListView.builder(
+ key: const PageStorageKey('liveList'), // keeps scroll offset
+ itemCount : liveMatches.length,
+ itemExtent : 160, // fixed card height (adjust to real value)
+ cacheExtent: 800, // pre-fetch ≈ 5 cards beyond viewport
+ itemBuilder: (ctx, i) =>
+ MatchCardFactory.createMatchCard(liveMatches[i]),
+);
Why this counts as a micro-optimisation
- Change is local to one widget-builder call; no new classes or structural refactor.
- Overall algorithmic complexity is unchanged; only hints are provided to the framework.
-
itemExtent
avoids per-item height measurement during scroll, andcacheExtent
instructs the engine to build a handful of off-screen items in advance. - Net effect: less layout work and smoother scrolling with zero impact on app logic—precisely the intent of a micro-level performance tweak.
Screenshots Before Micro-optimisations:
Screenshots After Micro-Optimisations:
Metric | Before (ListView.builder ) |
After (itemExtent + cacheExtent ) |
Δ |
---|---|---|---|
Avg UI frame time | ~12 ms | ~8 ms | −33 % |
Jank frames (>16 ms) in 30 s | 10–12 spikes | 1–2 spikes | −83 % |
Effective FPS | ~59 FPS | ~60 FPS |
Screenshots Before Micro-optimisations:
Screenshots After Micro-optimisations:
Results:
Metric | Baseline | After | Δ |
---|---|---|---|
fetchMatches execution time | 86.3 ms | 6.2 ms | ↓ 93% |
insertRecommendedBets (is called by fetchRecommendedBets) | 12.9 ms | < 3.1 ms | ↓ ≥ 76% |
-
Real-Time Betting prompt Trigger
- Uses geofence events and live-game status to display a lightweight “Quick Bet” overlay as soon as a user enters or approaches a stadium during a match.
-
Context-Aware notifications about ongoing or upcoming sports events
- Monitors the synced sports calendar and GPS proximity; sends push alerts for upcoming or ongoing events when the user is within 500 m of a venue.
-
Smart Bet Recommendations identifying repeated bets on the same team and generate personalized bet recommendations
- Counts a user’s repeated bets per team and surfaces a “Recommended for you” card before that team’s next fixture, based solely on their bet history.
-
User authentication
- Secure email/password signup and login with token-based session management, persisting credentials in encrypted local storage.
-
External Sports Calendar integration
- Fetches and merges varsity schedules from third-party APIs into the in-app calendar on a daily sync, keeping all game times up to date.
-
Bet selection & Bet Slip Confirmation
- Tapping a match opens a slip where users pick odds and wager details; a single “Confirm” tap stores the bet locally and queues it for sync when online.
-
Bookmark support for favorite matches.
-
Allows users to “star” matches in the calendar; starred items can be filtered to locate favorite matches faster.
-
User bet History
- Displays a list of every wager (team, sport, date).
-
Implemented In-App Merchandise Marketplace:
- Displays a native catalog of official university gear and items.
- Displays product name, price, image, and category for each item in a scrollable list.
- Retrieves product listings from the backend API and renders them in-app.
-
Implemented Product Detail View:
- Displays product information including images, price, description, and view count. Leverages view count visibility to reinforce purchase intent.
- Tracks each user's product views locally enabling personalized recommendations and improved product relevance.
-
Shopping cart:
- Allows users to mark products they intend to buy, storing those selections locally on the device.
Sprint | BQ Type | Business Question |
---|---|---|
4 | 3 | Which features do users attempt to access but abandon before completing an action, and what might be causing this behavior? |
4 | 4 | Which marketplace brands or product categories receive the highest number of product views? |
4 | 4 | We are a marketplace that allows universities to offer branded merchandise. What are the most popular university teams among students based on their betting activity? |
2 | 2 | Does the user seem to be at a university sports venue during a game? |
2 | 2 | Does the user seem to be near a university sports event? |
2 | 2 | Has the user placed multiple bets on the same team recently? |
3 | 1 | How many times have connection errors occurred across all endpoints in the last week? |
3 | 1 | What is the average response time for key API endpoints over the last week, and are there any noticeable slowdowns? |
3 | 2 | What sports events series gather the least attention from our users? |
The Team Popularity Dashboard presents a consolidated overview of student betting behavior by ranking university teams according to both total stakes and bet frequency. It features two side-by-side tables: the first orders teams by cumulative wagered amounts and shows the corresponding number of bets, while the second ranks teams by the total count of individual bets and displays the aggregate stake for each. This dual-perspective analysis highlights which university brands command the greatest engagement in the marketplace and informs strategic decisions about the selection and promotion of branded merchandise. The dashboard is accessible at the /analytics/dashboard/team-popularity/ endpoint.
The Product Views Dashboard provides two clear, side-by-side bar charts that surface exactly which items and categories students interact with most in the marketplace—ranking the top 10 products by individual view count and aggregating views by category (e.g. Ropa, Hogar, Juguetes). These insights empower our partnerships team to demonstrate real engagement metrics to third-party vendors and sponsors, enabling more targeted co-promotions and revenue-share agreements. You can access it at the /analytics/product-views/ endpoint.
Which features do users attempt to access but abandon before completing an action, and what might be causing this behavior?
The Incomplete Actions Dashboard highlights exactly which flows users begin but don’t finish—showing, for example, counts of incomplete_bet and incomplete_registration over the last day. By surfacing these abandonment metrics in a simple bar chart, the product team can pinpoint friction points (e.g. a confusing bet slip UI or a cumbersome signup form) and prioritize targeted UX fixes. Reducing these drop-offs boosts overall engagement, completed bets, and user acquisition—making the app more valuable for both students and our institutional partners. You can view it at the /analytics/incomplete-actions/ endpoint.
-
Product View Synchronization:
Product view events are captured locally when the user is offline and synchronized with the backend as soon as connectivity is restored. This synchronization occurs automatically upon detecting network reconnection. -
Product Data Caching:
The app checks connectivity status before making network requests. If offline, it fetches product data directly from local storage instead of making HTTP requests. Product data fetched from the backend is cached locally using SQLite. Cached data is displayed when offline in the Market Place View, ensuring continuous user experience and reducing backend requests upon reconnection - Prevent Failed Checkouts in Cart Screen: - We use an offline-first, connectivity-aware flow: all cart actions (add/remove/clear) write immediately to SQLite, and the “BUY NOW” button is disabled when offline—showing a friendly snackbar instead of attempting checkout—so we prevent failed network calls, avoid data loss or duplicate orders, and keep the UI predictable even on flaky connections.
-
Product Views Local Storage:
Product views are initially stored in an SQLite database on the device. Asynced
flag is maintained in each record to track synchronization status with the backend. Data remains persistent until synchronization is successfully completed. -
Persistent Storage with SQLite:
Product data is persistently stored using SQLite via thesqflite
package. This ensures products are accessible offline and improves app resilience.
-
Asynchronous Task Handling for product views:
Long-running tasks, such as syncing product views and database operations, are managed asynchronously to ensure the UI remains responsive. Operations leverage Flutter’s built-in async/await mechanism to handle tasks without blocking the main thread. -
Asynchronous operations for products Handled using Futures with
async/await
for network requests (fetchProducts()
), database operations (insertProduct()
), and caching mechanisms (in-memory caching inProductRepository
). Background JSON parsing is offloaded to isolates usingcompute()
to maintain UI responsiveness during heavy data processing tasks. Error handling (try-catch-finally
) ensures robustness, proper fallback to local storage on connectivity failures, and detailed API metrics logging. -
Asynchronous I/O for Cart Items: Asynchronous I/O: all database reads/writes (
_loadCartItems()
,addToCart()
,removeFromCart()
,clearCart()
) and HTTP checkout calls (create()
) useasync
/await
, keeping the main thread free for a responsive UI.
-
In-memory Cache for Product Data:
Implements a time-based caching mechanism with a 10-minute expiration to quickly retrieve product data without unnecessary network requests. The cache is invalidated after the defined period to maintain data freshness. -
Image caching strategy for Products : Uses
cached_network_image
to locally cache product images in theProductDetailPage
, enhancing performance by avoiding redundant network requests and improving offline experience.
-
David:
- Implement BQ question "What are the most popular university teams among students based on their betting activity "
- Implemented Cart View for marketplace
- Local Storage for Cart items
- Modified Product Detail to add products to the Cart
- EC Strategy for items of Cart View
- Uses asynchronous operations for CartViewModel
-
Sebastián:
- Implemented BQ "Which features do users attempt to access but abandon before completing an action, and what might be causing this behavior?"
- Implemented Marketplace view.
- Implemented caching and Local Storage Strategies for Market Place
- EC Strategy for Market Place
- Uses asynchronous operations for MarketPlaceViewModel.
-
Federico:
- Implemented Product Detail for market place
- Implemented local Storage and Caching Images for the ProductDetailView
- Implemented BQ "Which marketplace brands or product categories receive the highest number of product views"
- Use asynchronous operations for ProductDetailViewModel
- EC for views of ProductDetail