Sprint 4 - ISIS3510-Team14/Wiki GitHub Wiki


Flutter team:

  • Maria Alejandra Estrada Garcia
  • Marilyn Stephany Joven Fonseca
  • David Cuevas Alba

Swift Team:

  • Ernesto José Duarte
  • Lina Gómez
  • Eduardo Herrera

In this sprint, we will continue building functionalities and BQs inyourapp and dashboard. Moreover, we are going to includemulti-threading/asynchronous strategies for threading/concurrencyand micro-optimization strategies.

Value Proposition

Empowering Sustainable Habits through Gamification and Accessibility

The app’s value proposition is centered on gamifying recycling to build sustainable habits while making recycling accessible, engaging, and educational. By leveraging features like user streaks, point allocation, leaderboards, and the campus recycling map, the app creates a community-driven experience that motivates users to recycle consistently.

Justification for the Value Proposition

The app's value proposition is rooted in its ability to gamify recycling, making the process not only habitual but also rewarding and accessible. This justification combines the features, business questions, revenue model, and collected data to show how the app delivers value to users, the university, and sustainability initiatives.

Features Driving User Engagement and Habit Formation

The app leverages a streak tracking system, point allocation for recycling, and a leaderboard to build a strong motivation loop for users. These features encourage daily recycling while creating a sense of achievement through visible progress and friendly competition. The campus recycling map adds accessibility by addressing a common barrier—locating recycling bins—while the camera verification feature ensures that recycling actions are legitimate, enhancing the trustworthiness of the leaderboard.

Business Questions Guiding Optimization

By addressing questions like "How effective are streak reminders?" and "What are the most recycled objects?", the app continuously evaluates its ability to engage users and educate them on sustainable practices. For example, insights from "How frequently do users return within 24 hours of their last login?" help improve retention strategies, ensuring that the app remains an integral part of the user's daily routine.

Revenue Model Supporting Sustainability

The app’s university sponsorship model aligns with institutional green initiatives, where the university funds the app to improve campus-wide recycling rates. Additionally, by providing data insights—such as the most visited recycling points or peak recycling times—the app justifies its value as a tool for optimizing campus waste management systems.

Data as the Foundation of Impact

The app collects and utilizes data to inform both its features and its broader impact:

  • Recycling habits are tracked through streaks and activity calendars, providing users with visual motivation while delivering actionable insights to campus planners.
  • Leaderboard metrics foster community engagement and highlight exemplary recyclers, while also enabling friendly competition.
  • User preferences captured through map interactions and camera scans help refine the app's usability, ensuring it meets user needs.

Seamless User Experience with Connectivity Resilience

The app’s eventual connectivity strategies, such as caching the leaderboard and saving streak data locally, ensure that core functionalities remain accessible even without internet access. This approach improves user satisfaction and solidifies the app's position as a reliable tool for promoting recycling behaviors.

By integrating these elements, the app delivers on its promise to empower users to adopt sustainable habits, support the university’s environmental goals, and create a gamified, community-driven experience that resonates with modern campus life.

Features

Sprint 2

This are the implemented features for both apps:

Campus Recycling Map:

  • An interactive map that highlights the locations of recycling bins and other waste disposal points across the campus.
  • Users can find the nearest recycling bin, promoting convenience and accessibility.

App authentication

  • An interactive GUI that allows the user to log in and log out of the app using Auth0.

Verification through Camera:

  • Image recognition feature that uses the phone’s camera to verify that users are properly recycling.
  • Users must scan their recycling actions, and points are only awarded after successful verification, ensuring accountability.

Sprint 3

This are the implemented features for both apps:

Campus Recycling Map:

  • An interactive map that highlights the locations of recycling bins and other waste disposal points across the campus.
  • Users can find the nearest recycling bin, promoting convenience and accessibility.

App authentication

  • An interactive GUI that allows the user to log in and log out of the app using Auth0.

Verification through Camera:

  • Image recognition feature that uses the phone’s camera to verify that users are properly recycling.
  • Users must scan their recycling actions, and points are only awarded after successful verification, ensuring accountability.

Notifications:

  • Daily Reminder to Recycle: This notification can be scheduled to go off every day at a specific time.
  • End of Day Streak Reminder: This notification can be scheduled towards the end of the day to remind users to recycle and keep their streak.

Recycle view:

The Recycle View feature allows users to view recycling information, displaying the latest information when online and local data when offline, ensuring uninterrupted access.

Sprint 4

Scoreboard: Ranking of Users by Points

A feature that displays a leaderboard of users ranked by their recycling points.

  • Multi-threading/Concurrency: Fetch and sort the list of users by points using a multi-threaded approach to ensure smooth performance and faster data processing.
  • Caching: Only the top 25 users are displayed, with the data being cached locally to improve load times and reduce redundant calls to Firebase.
  • Eventual Connectivity (EC): The scoreboard updates automatically when the app detects internet connectivity, ensuring the latest scores are reflected.
  • View: Scoreboard

Point Allocation for Recycled Objects

Users earn points for recycling items, which are reported to Firebase for tracking and leaderboard updates.

  • Eventual Connectivity (EC): Points are awarded and logged in Firebase only when an image of the recycled object is scanned while the device is connected to the internet. If offline, the system queues the action for future submission.
  • Multi-threading/Concurrency: Scanning and uploading the image use concurrent threads to avoid blocking the main UI thread.
  • View: Profile

Streak Tracking: Calendar of Recycling Activity

A calendar view displays the days a user has recycled, helping users track their streaks and stay motivated.

  • Local Storage: Recycling activity data is saved locally to ensure offline access and smooth loading.
  • Eventual Connectivity (EC): If there’s no internet, the calendar shows locally stored data. Once connected, the app synchronizes data with the server to ensure consistency.
  • View: Calendar

Business questions (BQ)

Sprint 2

Type 1 - App Telemetry (Performance):

  • How many users access the app by platform (Swift or Flutter) per hour of the day? This is a type 1 question because it analyzes the peaks of app usage by platform and hour, helping to manage service availability and resource allocation.

Type 2 - User Experience Improvement:

  • What is the time the app takes to scan an object and show the recommendation? This is a type 2 question because it directly improves the user experience by measuring and optimizing the efficiency of the scanning process in the app.

Type 3 - Feature Analysis (Functionality):

  • How many times do the users consult the recycle map section to locate the recycling points? This is a type 3 question because the feature might be upgraded to be more appealing or useful for the user.

Type 4 - Data-Derived Insights:

  • What is the most visited location to recycle on campus? This is a type 4 question because it focuses on identifying the strategic recycling locations on campus that students use the most. This information can help improve these locations.

  • Which platform (iOS or Android) has been the most used overtime? This is is a type 4 question because it uses platform usage data (iOS vs Android) to inform strategic decisions about future resource allocation. By analyzing historical trends, the business can optimize development efforts, feature improvements, and support for the most-used platform, leading to more efficient resource utilization and potentially maximizing profit.

  • What are the most frequently recycled objects? This is a type 4 question because it focuses on understanding recycling patterns, which can help improve the placement of recycling bins and design educational initiatives based on recycling habits.

Sprint 3

Type 2 - User Experience Improvement:

  • What are the closest recycling points?: It’s type 2 because the user is going to interact with the map displayed on the app.

  • How effective is the ‘Daily Reminder to Recycle’ notification in encouraging users to login within the same day?: This is a type 2 question because it measures user interaction with the daily notification by tracking how many users log a recycling activity after receiving it. The data helps optimize notification timing to maximize engagement, and the notifications are shown to the user.

Type 3 - Feature Analysis (Functionality):

  • How frequently do users return to the app within 24 hours of their last login?: This is a type 3 question because it focuses on understanding user engagement with the app's core functionality. By analyzing this return frequency, the business can gauge the app's ability to retain users and make data-driven decisions on features or improvements that may increase user retention and re-engagement.

  • What percentage of users access the map through the navbar vs. the main page button?: This is a type 3 question because it focuses on understanding user engagement with the app's flow. By analyzing this return frequency (fraction), the business can measure the most accessible way of entering this feature and consider changes on the user interface given the proportions of usage.

  • How frequently do users choose to classify their garbage through the scan feature versus consulting the recycle information section?: This is a Type 3 question because it helps analyze user preferences and engagement with the app's core functionalities. By understanding the proportion of users who prefer the scan feature over the recycle information section, the app team can assess the value of each feature and explore potential enhancements based on user behavior.

Type 5 (*) Multi-type:

  • What recycling information should we add or update based on user engagement data to different trash types to make the information more effective?: This is a Type 5 question, combining Type 3 (New or updated features) and Type 4 (Benefits from data). It focuses on analyzing user engagement data to identify which recycling information tips is the most effective and which need improvement, guiding updates or additions to enhance the feature's educational impact.

Sprint 4

Type 1 - App Telemetry (Performance):

  • What is the average load time for viewing and editing a user's profile?:
    This is a type 1 question because it focuses on identifying performance bottlenecks in profile-related actions, ensuring a smooth and responsive user experience.

Type 2 - User Experience Improvement:

  • How many points does a user earn after a scan event?:
    This is a Type 2 business question because it directly informs the user about the points earned after a scan event, thereby improving the user experience by providing immediate feedback and engagement.

  • How many days and which days has the user recycled? This is type 2 because it is displayed on the home sceen and history view as part of the streak to motivate the user to use more the app.

  • How many consecutive days has a user maintained their streak?
    This is a Type 2 business question because it directly informs the user about the number of consecutive days they’ve maintained their streak, providing immediate feedback on their engagement. This visibility helps motivate the user to keep up their sustainable habits, as they can track their progress visually in the app.

  • Who has the highest score in SustainU? It’s type 2 because the user is going to interact with the scoreboard displayed on the app.

Type 3 - Feature Analysis (Functionality):

  • How much personal information do users provide (if they fill in their career, semester, or leave it blank)?: This is a Type 3 question because it helps evaluate and optimize the feature by analyzing user behavior, potentially leading to changes in the profile interface and it helps decide on features related to user data collection and privacy.

Type 4 - data derived insights

  • What percentage of users actively maintain their streak by logging recycling activities for at least seven consecutive days?:
    This is a type 4 question because it evaluates how well the streak feature motivates users to engage consistently, helping to enhance its effectiveness.

Type 5 (*) Multi-type:

  • What is the relationship between maintaining a streak and increased usage of other features, such as checking the leaderboard or recycling map?:
    This is a type 5 question because it combines Type 2 (user engagement analysis) and Type 3 (feature engagement evaluation) to determine if streak tracking influences broader app usage and engagement.

Micro-optimization Strategies

Swift

Performance Improvements in the Camera Feature

After implementing micro-optimizations in the camera feature, a clear reduction in CPU usage is observed. The "before" analysis demonstrates higher CPU load, particularly in the CPU Total Load and CPU User Load, indicating less efficient resource allocation. Following the optimizations, the "after" analysis reveals a notable decrease in these metrics, showcasing improved efficiency and reduced processing overhead. These optimizations result in a smoother and more responsive user experience while lowering energy consumption, critical for mobile devices.

Before Optimization

WhatsApp Image 2024-12-02 at 8 12 08 PM

After Optimization

WhatsApp Image 2024-12-02 at 8 43 40 PM

Performance Improvements in the Map Feature

Following micro-optimizations applied to the map feature, there is a noticeable reduction in CPU usage and resource consumption. In the "before" analysis, the map feature displayed elevated CPU load and higher energy demands, particularly in the CPU Total Load. The "after" analysis shows a significant reduction in these metrics, highlighting enhanced efficiency in rendering and interaction processes. These improvements contribute to better performance and a more energy-efficient experience for users.

Before Profiling

WhatsApp Image 2024-12-02 at 8 10 10 PM

After profiling

WhatsApp Image 2024-12-02 at 9 00 12 PM

Flutter

Before Micro-Optimization Profiling

Profile View

Memory view

A visual illustration of memory utilization over time is the memory chart which offers information about how the program uses and maintains memory. It enables developers to determine which area of the program is using memory by classifying memory usage into several portions, such as the Raster Layer, Flutter Native, and Dart Heap.

Key visual elements include pink triangles, which represent manual garbage collection (GC) or snapshots taken by the developer, and blue circles, which indicate automatic garbage collection triggered by the Dart Virtual Machine (VM). The chart also features dashed lines that illustrate the Resident Set Size (RSS) and allocated memory boundaries. These boundaries provide a clear picture of the memory allocated versus the memory currently in use.

profile

The image displays the memory profiling view in Flutter DevTools, highlighting an efficient memory management process. The memory usage is segmented into two key areas: new space, which handles short-lived objects with a usage of 1.2 MB (out of a 4.0 MB capacity), and old space, which stores long-lived objects, using 91.4 MB (close to its 99.1 MB capacity). The garbage collection (GC) activity shows a total of 14 collections, with minimal latencies of 6 ms for new space and 8 ms for old space, reflecting efficient cleanup processes. The blue circles on the chart represent automatic GCs triggered by the Dart VM, ensuring stability in memory usage.

Memory stability is evident from the consistent horizontal lines in the graph, with no significant spikes or drops. The Resident Set Size (RSS), shown by the dashed orange line, and the allocated memory, represented by the dashed blue line, remain steady, indicating no signs of memory leaks or excessive allocation. The pink triangles on the chart denote manual GC snapshots, which are valuable for debugging memory allocations at specific intervals. Additionally, the console logs confirm that the app is loading user data first from the cache and then from the server, aligning with the observed consistent memory activity as no significant memory spikes occur during this process.

profile2

This second image focuses on the performance profiling view, showcasing the application's frame rendering performance. The chart tracks frame rendering times in milliseconds (ms), with blue bars representing UI and raster times for each frame. All the frame times remain below the 16 ms threshold required to maintain a smooth 60 FPS (frames per second), indicating fluid rendering with no noticeable lag. The average FPS is displayed as 57, which, while slightly below the ideal target of 60 FPS, suggests strong performance with minimal jank (slow frames). Notably, the absence of red bars in the chart indicates that the app does not suffer from significant delays caused by jank or shader compilation, ensuring a responsive user experience. Although no specific frame is selected for detailed analysis, developers have the option to drill into timeline events to investigate individual frames for tasks like layout building or widget rebuilding, further optimizing performance if needed.

Performance view

Profile Performance before

The performance chart reveals significant jank (slow frames), with multiple red bars indicating frames exceeding the 16ms threshold for 60 FPS rendering. The average frame rate is 32 FPS, far below the desired target of 60 FPS, suggesting the app struggles with smooth rendering. This jank is primarily caused by both UI thread delays (blue bars) and raster thread delays (orange bars).

The red overlay in certain frames highlights shader compilation jank, with 13 frames affected. These frames take excessive time due to the initial rendering of shaders, which happens when the app dynamically compiles graphical effects. One frame, in particular, experienced 18.5ms of shader compilation time, contributing significantly to overall jank.

The combination of UI thread jank, raster thread delays, and shader compilation results in visible stutters and delays in the Profile screen. This degrades the responsiveness of the application, leading to a poor user experience.

CPU Profiling

Profile CPU

The CPU Flame Chart showcases the distribution of CPU resources during the rendering of the Profile View. The chart reveals a duration of 2.8 seconds, highlighting significant activity in handling UI updates and rendering processes. Key areas of resource consumption include pointer data dispatch, widget rebuilding, layout recalculations, and painting operations.

The initial portion of the chart is dominated by _dispatchPointerDataPacket and related gesture handling functions. This indicates that user interactions or pointer events are being processed. The efficient handling of these events is crucial for ensuring responsive user interactions.

On the other hand, the flame chart shows substantial activity under SchedulerBinding.handleDrawFrame and subsequent widget rebuilding processes like Element.rebuild and StatefulElement.performRebuild. This signifies that the app is updating widgets dynamically, likely due to user-triggered actions or changes in the Profile View data. However, excessive rebuilding can indicate inefficiencies in the widget tree structure. However, the repeated calls to Element.rebuild suggest that the Profile View might be triggering unnecessary widget updates. Optimizing this process can reduce rebuild costs and improve performance.


Home View

Memory view

The total memory usage is recorded at 92.6 MB, which is well within the allocated capacity of 103.1 MB. This demonstrates that memory is being utilized efficiently without unnecessary over-allocation. The memory is divided into two main areas: New Space for short-lived objects and Old Space for long-lived objects.

homeM

The new space shows a usage of 1.2 MB out of a capacity of 4.0 MB, with four garbage collection cycles occurring during the session. The garbage collection latency is impressively low at 2 ms, ensuring that temporary objects, such as transient UI elements or lightweight computations, are promptly and efficiently cleaned up without impacting performance.

However, the old space holds 91.4 MB of memory, close to its capacity of 99.1 MB. This is indicative of long-lived objects, such as UI components, background services, or cached data, being effectively managed. Ten garbage collection cycles occurred in the old space with an average latency of 8 ms, which is efficient for cleaning up persistent data structures without affecting the application's responsiveness.

On the other hand, it shows that it performed a total of 14 garbage collection cycles during the profiling session, split between new and old spaces. The garbage collection latency averages around 6 ms, showcasing that memory cleanup is quick and non-intrusive. This efficiency ensures the application remains stable and responsive during user interactions.

Hence, the app demonstrates its ability to maintain stable memory usage even while handling background processes such as notification permissions and FCM token generation. This scalability ensures the app can handle additional features or higher data volumes without performance degradation. Additionally, the absence of memory spikes and consistent garbage collection latency show that the app is free from memory leaks or unnecessary object creation. As a result, users are unlikely to experience lags or crashes, contributing to a smooth and seamless user experience.

Performance view

Important details about the frame rendering procedure are revealed by the Flutter DevTools' performance view of the Home screen. Two bars are used to represent each frame: orange bars for the raster thread and blue bars for the user interface thread. Red overlays represent frames that above the crucial threshold of 16 milliseconds needed for 60 frames per second performance, and these bars show how long it takes each thread to finish its job. More time spent on either thread causes jank, which makes interactions or animations stutter.

homep

Several frames in the chart show red overlays, indicating janky frames that took longer than 16 milliseconds to render. While the blue bars (UI thread) remain relatively small in most cases, the orange bars (raster thread) display significant spikes for certain frames. This suggests that the bottleneck in performance is primarily on the raster thread, which handles GPU-intensive operations such as rendering shadows, gradients, or clipping. These spikes could be caused by expensive graphics operations or complex UI layouts.

The absence of dark red bars in the chart confirms that no new shader compilations occurred during the session. This is a positive sign, as shader compilations often cause significant jank when first triggered. However, the average FPS for the session is 57, slightly below the target of 60 FPS. While this indicates stable performance overall, the janky frames reduce the smoothness of animations and user interactions, which can impact the user experience.

Reducing janky frames and optimizing speed require addressing the workload of the raster thread. The processing time of the raster thread can be decreased by minimizing GPU-intensive tasks like excessive shadows, gradients, or overlapping layers (overdraw). Finding overdraw problems can be aided by tools such as the Flutter Performance Overlay. Additionally, the UI thread's effort can be reduced and the raster thread indirectly benefits from optimizing the widget tree by minimizing nesting and reusing widgets with const. Performance can also be improved by batching UI updates and making effective use of caching.


Scoreboard

Memory view

image Here we can see stable memory usage, with total memory at 92.6 MB out of 103.1 MB, and most allocations in the Old Space (91.4 MB), indicating long-lived objects. Garbage collection (GC) is efficient, with 14 collections (4 in New Space at 2 ms latency and 10 in Old Space at 8 ms). The memory chart remains stable, with no significant spikes or leaks, and manual GC events (purple triangles) align with user interactions, such as loading user data from cache and server. Overall, memory management appears effective, though optimizing Old Space usage could further improve performance.

Performance view

image

The Flutter DevTools performance view indicates consistently smooth rendering, with frame times holding steady below 7 ms, well within the 16 ms threshold required for 60 FPS. This demonstrates efficient rendering and minimal performance bottlenecks, ensuring a responsive user interface. The steady frame times suggest optimized widget tree management and no noticeable jank, even during interactions or animations. Overall, the app's performance is highly stable, providing a seamless user experience without frame drops or lag.


Recycle View

Memory View

recyclem

Demonstrates steady memory usage of 92.6 MB, staying well within the allocated capacity of 103.1 MB. This reflects efficient resource usage without over-allocation. In the New Space, temporary objects occupy 1.2 MB with a capacity of 4.0 MB, and four garbage collection (GC) cycles occur with a latency of 2 ms, ensuring short-lived objects are cleaned quickly. Meanwhile, the Old Space, holding long-lived objects like persistent UI components, uses 91.4 MB out of 99.1 MB, with ten GC cycles at 8 ms latency.

A total of 14 GC cycles (four in New Space and ten in Old Space) keep memory usage under control. The overall latency of 6 ms highlights non-intrusive and efficient cleanup. Blue circles in the chart indicate automatic GC events by Dart VM, while pink triangles represent manual snapshots for debugging. These markers show regular and consistent memory cleanup, avoiding memory bloat.

The memory chart shows stable RSS (Resident Set Size), represented by the orange dashed line, and consistent allocated memory, represented by the blue dashed line. The absence of spikes confirms there are no memory leaks or unexpected allocations. This stability ensures the Recycle View runs smoothly without interruptions.

Performance view

Recycle Performance before

The performance view for the Recycle View reveals severe jank, with almost every frame exceeding the 16ms threshold required for 60 FPS rendering. The average frame rate is a dismal 11 FPS, indicating that the app is significantly struggling to maintain smooth rendering. This level of performance would result in a highly unresponsive and laggy user experience.

The majority of the frame rendering time is spent on the raster thread, as indicated by the tall orange bars. This suggests that graphical operations, such as drawing or compositing, are consuming excessive resources. These delays could be caused by inefficiencies in the graphical pipeline, such as overlapping opacities, excessive clipping, or complex shadows.

Similar to the Profile View before optimization, shader compilation jank is present, with 13 frames affected and a total of 410.5ms spent on shader compilation. Shader compilation happens when graphical effects are rendered for the first time, and this delay is significant. It indicates a lack of pre-compiled shaders or overly complex visual effects in the Recycle View.

Also, the blue bars indicate UI thread delays, though they are less pronounced compared to raster thread delays. However, combined with the raster issues, they contribute to the overall jank. This suggests that while widget rebuilding on the UI thread is not the primary bottleneck, inefficient rendering on the raster thread overshadows the entire process.


Map View ca28a538-6212-4049-a153-17dfaa1017d4

The Map View shows clear signs of jank. The build phase is the longest UI phase in this frame, taking 123.6ms, which exceeds the 16ms frame budget needed for smooth 60 FPS animations. Additionally, the raster phase is also problematic, consuming 312.8ms, indicating inefficiencies in rendering. Widgets such as ListTile, Image, and Text are identified as the most frequently rebuilt widgets in this frame. The repeated rebuilding of these elements significantly contributes to the jank.

6334b538-e00a-470c-a1ea-0759e06272c2

The performance overlay analysis shows high raster and UI times. In the top graph, raster times peak at 32.6ms per frame on average, indicating significant time spent on painting operations such as rendering images and other visual elements. In the bottom graph, the UI phase shows average times of 81.6ms per frame, highlighting expensive rebuild operations in widgets like ListView, Image, Text, and GestureDetector, especially within the draggable list. The high rebuild count—approximately 10 rebuilds per frame—adds to the performance issues.

High raster times indicate inefficiencies in rendering operations, particularly for painting visual elements such as images or shadows. The build phase also reveals expensive UI operations due to frequent widget rebuilds, especially in commonly used widgets such as ListTile, Image, and Text. With 10 rebuilds per frame, performance degradation is evident.

Micro-optimization strategies

Micro-Optimization #1: Avoid Creating Unnecessary Objects in Loops

For the profile view, to enhance performance, variables like _localData, which assign data from Firestore in the _loadUserProfile method, should be pre-initialized outside of loops or blocks. This reduces the overhead of repeatedly creating new instances during iteration, leading to more efficient memory usage and faster execution. At the same time, to minimize redundant UI rebuilds in methods like _saveProfile or _loadUserProfile, consolidate setState calls. Grouping updates and invoking setState once ensures the widget tree rebuilds only when necessary, improving the app’s responsiveness and reducing computational load.

This is the code before the optimization:

Future<UserProfile> _loadUserProfile() async {
  // Obtener las credenciales del usuario repetidamente
  Map<String, dynamic>? credentials = await _storageService.getUserCredentials();
  String email = credentials?['email'] ?? 'Unknown Email';

  // Obtener los documentos de Firestore sin consolidar operaciones
  DocumentSnapshot userDoc = await _firestore.collection('users').doc(email).get();
  DocumentSnapshot userInfoDoc = await _firestore.collection('users_info').doc(email).get();

  // Verificar si el documento existe y crear objetos nuevos para manejarlo
  if (!userInfoDoc.exists) {
    Map<String, String> defaultData = {
      'career': '',
      'semester': '',
    };
    await _firestore.collection('users_info').doc(email).set(defaultData);
  }

  // Crear un nuevo mapa por cada acceso de datos
  Map<String, dynamic> userData = {};
  userData.addAll(userDoc.data() ?? {});

  Map<String, dynamic> userInfoData = {};
  userInfoData.addAll(userInfoDoc.data() ?? {});

  // Repetidamente crear objetos de tipo `String` para asignar valores
  String career = '';
  String semester = '';

  if (userInfoData.containsKey('career')) {
    career = userInfoData['career'] ?? '';
  }

  if (userInfoData.containsKey('semester')) {
    semester = userInfoData['semester'] ?? '';
  }

  // Actualizar controladores de texto usando variables separadas
  _careerController.text = career;
  _semesterController.text = semester;

  // Crear un objeto nuevo en cada asignación del local data. 
  Map<String, dynamic> localData = {};
  localData['nickname'] = credentials?['nickname'] ?? 'Unknown User';
  localData['email'] = email;
  localData['profilePicture'] = credentials?['picture'];
  localData['totalPoints'] = userData.containsKey('points') ? userData['points']?['total'] ?? 0 : 0;
  localData['career'] = career;
  localData['semester'] = semester;

  // Repetir llamadas para crear nuevos objetos en lugar de reutilizarlos
  String nickname = localData['nickname'] ?? 'Unknown User';
  String profilePicture = localData['profilePicture'] ?? '';

  // Crear y retornar el perfil con nuevas referencias, por cada vez que entra. 
  return UserProfile(
    nickname: nickname,
    email: localData['email'],
    profilePicture: profilePicture,
    totalPoints: localData['totalPoints'],
    career: localData['career'],
    semester: localData['semester'],
  );
}

Future<void> _saveProfile(String email, String? career, String? semester) async {
  // Revisar la conectividad varias veces y crear nuevos objetos en cada verificación
  Connectivity connectivity = Connectivity();
  var connectivityResult = await connectivity.checkConnectivity();

  if (connectivityResult != ConnectivityResult.mobile &&
      connectivityResult != ConnectivityResult.wifi) {
    _showNoInternetMessage();
    return;
  }

  try {
    // Crear múltiples referencias de objetos en lugar de consolidarlas
    Map<String, dynamic> newData = {};
    newData['career'] = career;
    newData['semester'] = semester;

    await _firestore.collection('users_info').doc(email).set(newData, SetOptions(merge: true));

    // Crear referencias múltiples y redundantes para actualizar datos locales
    Map<String, String?> updatedLocalData = {};
    updatedLocalData['career'] = career;
    updatedLocalData['semester'] = semester;

    // Repetir asignaciones y llamadas redundantes
    setState(() {
      _localData['career'] = updatedLocalData['career'];
      _localData['semester'] = updatedLocalData['semester'];

      Map<String, dynamic> userProfileData = {};
      userProfileData['nickname'] = _localData['nickname'];
      userProfileData['email'] = _localData['email'];
      userProfileData['profilePicture'] = _localData['profilePicture'];
      userProfileData['totalPoints'] = _localData['totalPoints'];
      userProfileData['career'] = updatedLocalData['career'];
      userProfileData['semester'] = updatedLocalData['semester'];

      _userProfile = Future.value(UserProfile(
        nickname: userProfileData['nickname'],
        email: userProfileData['email'],
        profilePicture: userProfileData['profilePicture'],
        totalPoints: userProfileData['totalPoints'],
        career: userProfileData['career'],
        semester: userProfileData['semester'],
      ));
    });

    // Crear objetos repetidos para mostrar mensajes
    String successMessage = 'Profile updated successfully!';
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(successMessage)),
    );
  } catch (e) {
    // Manejo de errores sin consolidar
    String errorMessage = 'Failed to update profile. Please try again.';
    print('Error: $e');
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(errorMessage)),
    );
  }
}

This is after the optimization:

Future<UserProfile> _loadUserProfile() async {
  // Pre-declare and reuse variables to avoid redundant object creation
  Map<String, dynamic>? credentials = await _storageService.getUserCredentials();
  String email = credentials?['email'] ?? 'Unknown Email';

  DocumentSnapshot userDoc = await _firestore.collection('users').doc(email).get();
  DocumentSnapshot userInfoDoc = await _firestore.collection('users_info').doc(email).get();

  if (!userInfoDoc.exists) {
    // Initialize default values only once when the document does not exist
    await _firestore.collection('users_info').doc(email).set({
      'career': '',
      'semester': '',
    });
  }

  // Consolidate data extraction
  Map<String, dynamic> userData = userDoc.data() ?? {};
  Map<String, dynamic> userInfoData = userInfoDoc.data() ?? {};

  // Update controller values directly without creating new objects
  _careerController.text = userInfoData['career'] ?? '';
  _semesterController.text = userInfoData['semester'] ?? '';

  // Update local data efficiently
  _localData = {
    'nickname': credentials?['nickname'] ?? 'Unknown User',
    'email': email,
    'profilePicture': credentials?['picture'],
    'totalPoints': userData['points']?['total'] ?? 0,
    'career': _careerController.text,
    'semester': _semesterController.text,
  };

  // Return updated profile without redundant variables
  return UserProfile(
    nickname: _localData['nickname'],
    email: _localData['email'],
    profilePicture: _localData['profilePicture'],
    totalPoints: _localData['totalPoints'],
    career: _localData['career'],
    semester: _localData['semester'],
  );
}

Future<void> _saveProfile(String email, String? career, String? semester) async {
  // Check for connectivity before proceeding
  if (!_isConnected) {
    _showNoInternetMessage();
    return;
  }

  try {
    // Consolidate Firestore write operation to prevent redundant object creation
    await _firestore.collection('users_info').doc(email).set({
      'career': career,
      'semester': semester,
    }, SetOptions(merge: true));

    // Update local data efficiently
    _localData['career'] = career;
    _localData['semester'] = semester;

    // Avoid redundant setState calls
    setState(() {
      _userProfile = Future.value(UserProfile(
        nickname: _localData['nickname'],
        email: _localData['email'],
        profilePicture: _localData['profilePicture'],
        totalPoints: _localData['totalPoints'],
        career: _localData['career'],
        semester: _localData['semester'],
      ));
    });

    // Inform the user of success
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Profile updated successfully!')),
    );
  } catch (e) {
    // Catch and handle errors
    print('Failed to save profile: $e');
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Failed to update profile. Please try again.')),
    );
  }
}

The codes changed where, the optimized version reduces unnecessary object creation by reusing variables like userData and userInfoData and consolidating updates to _localData and controllers, leading to improved memory efficiency and execution speed. In _saveProfile, redundant calls to _loadUserProfile are replaced with direct state updates, minimizing UI rebuilds and ensuring smoother responsiveness. Firestore writes are streamlined with consolidated map updates, and cleaner logic improves readability and maintainability. Overall, the optimizations enhance performance, reduce overhead, and create a more efficient and responsive application.

In other words, to optimize memory usage and reduce the overhead of creating new objects, variables such as credentials, email, userData, and userInfoData are reused. This approach eliminates unnecessary object instantiations, improving performance. Additionally, _localData and controllers like _careerController and _semesterController were updated directly, avoiding the creation of intermediate variables for these updates.

To enhance UI performance, multiple setState calls in methods like _saveProfile and _loadUserProfile were consolidated into a single call. This reduces the frequency of UI rebuilds, improving the responsiveness of the application. Finally, redundant data assignments were removed to streamline operations. For instance, _careerController.text and _semesterController.text are now updated directly without relying on temporary variables. This makes the code cleaner and more efficient.

Micro-Optimization #4: Use indexed loops instead of iterator and for-each loop

This optimization can provide a slight performance improvement, especially when working with large datasets, as it:

  • Avoids the creation of iterators
  • Reduces the overhead of higher-order functions
  • Accesses elements directly by index

Code before:

filteredUsers = users
            .where((user) =>
                user.name.toLowerCase().contains(query.toLowerCase()))
            .toList();

Code after

List<User> filtered = [];
        String lowercaseQuery = query.toLowerCase();
        
        // Bucle indexado en lugar de where
        for (int i = 0; i < users.length; i++) {
          if (users[i].name.toLowerCase().contains(lowercaseQuery)) {
            filtered.add(users[i]);
          }
        }
        filteredUsers = filtered;

However, it is important to note that in this specific case, given the relatively small size of the data being handled, the performance improvement might be minimal.

Micro-Optimization #7: Use relative layouts or constraint layouts instead nested linear layouts Main optimizations:

  • Replacing SingleChildScrollView + Column with CustomScrollView improves scroll performance by allowing for more efficient handling of large, scrollable content.
  • Removing unnecessary Expanded reduces layout complexity and overhead, leading to a simpler and more efficient rendering process.
  • Using SliverList enhances scroll performance, particularly with large lists, by rendering only the visible items and reducing memory usage.
  • Reducing widget nesting helps streamline the layout, making the UI more responsive and easier to manage.
  • Optimizing space with SliverFillRemaining ensures that remaining space is used effectively, improving the visual layout without wasting resources.

Micro-Optimization #8: Outside of ListTile

Wrapping the ListTile in a RepaintBoundary helps isolate its rendering from the parent and sibling widgets. This ensures that when the ListTile updates, it does not trigger unnecessary repaints of the entire widget tree. This is particularly useful in lists or complex layouts where each widget may need independent updates.

child: RepaintBoundary(
  child: ListTile(
    ...
  ),
),

Micro-Optimization #9: Reducing Widget Nesting in subtitle Deeply nested widget trees can increase the cost of layout and rebuild operations. Instead of using a Column with multiple child widgets, we can simplify it with Text.rich, which allows combining multiple text styles in a single widget. This reduces the layout complexity and improves performance.

From this:

subtitle: Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text(point.description),
    const SizedBox(height: 5),
    GestureDetector(
      onTap: () {},
      child: const Text(
        "Recyclables and Organic",
        style: TextStyle(
          color: Color.fromRGBO(17, 144, 198, 1),
          fontWeight: FontWeight.bold,
        ),
      ),
    ),
  ],
),

To this:

subtitle: Text.rich(
  TextSpan(
    children: [
      TextSpan(
        text: "${point.description}\n",
        style: DefaultTextStyle.of(context).style,
      ),
      TextSpan(
        text: "Recyclables and Organic",
        style: const TextStyle(
          color: Color.fromRGBO(17, 144, 198, 1),
          fontWeight: FontWeight.bold,
        ),
      ),
    ],
  ),
),

After Micro-Optmization Profiling

Profile View

Memory view

Profile Memory After

After implementing the micro-optimization, the memory view demonstrates significant improvements in stability and efficient usage. The reduced frequency of garbage collection (GC) events highlights a better-managed object lifecycle, with fewer unnecessary objects being created and discarded during runtime. This not only minimizes memory fragmentation but also reduces GC overhead. The memory utilization is now consistent, as seen by the flat RSS (Resident Set Size) and allocated memory lines. These lines indicate that the app no longer suffers from spikes in memory usage, which were previously indicative of suboptimal memory allocation. Additionally, the total memory usage of 97.3 MB is well within the allocated capacity, ensuring no memory leaks or over-allocation issues.

Performance view

Profile Performance After

The performance view shows a remarkable reduction in jank (represented by red bars) and shader compilation delays compared to the previous state. The average frame rate has improved significantly, resulting in a smoother and more responsive user experience. By consolidating setState calls, the app avoids triggering excessive UI rebuilds, which previously caused frame delays and increased rendering overhead. Additionally, the absence of dark red bars indicates that shader compilation, which was previously causing performance bottlenecks, has been addressed through pre-compilation or optimized GPU usage. The workload is now evenly distributed between the UI thread and Raster thread, with frame times consistently below the critical 16ms threshold. These changes ensure that animations and transitions occur seamlessly, without visible lags or dropped frames.

CPU Profiling

Prodile CPU After

The effect of the optimizations on the Dart execution and rendering tasks of the application is reflected in the CPU profiler. Tasks like Element.rebuild take a lot less processing time, as seen by the flame chart's decreased frame build times. This suggests that needless computational effort has been reduced by eliminating the need for redundant widget rebuilds. Because of less nesting of activities and a more effective execution flow, the timeline's depth is likewise shallower. Additionally, procedures like _drawFrame and SchedulerBinding.handleDrawFrame exhibit faster and more efficient execution due to their lower duration. By avoiding the spikes that once caused slowdowns and unresponsive behavior, the total CPU resource utilization is more balanced.

Scoreboard

Memory view

image

The Flutter DevTools memory inspector shows a stable pattern, with most memory allocated to long-lived objects. Garbage collection remains efficient, with consistent behavior and no significant changes in latency or frequency. A recent micro-optimization was implemented, but it did not noticeably affect memory usage or allocation patterns. While the app’s memory management is already reliable, the minimal impact of the optimization suggests that more substantial adjustments might be needed to achieve further improvements.

Performance view

image

The Flutter DevTools performance view shows consistently smooth rendering, with frame times well below the threshold needed for smooth performance. This reflects efficient rendering and minimal bottlenecks, ensuring a responsive and fluid user interface. The stable frame times suggest well-optimized widget tree management and no noticeable jank, even during complex interactions or animations. Overall, the app's performance remains highly stable, providing a seamless experience without interruptions or delays. Despite implementing a micro-optimization, it had little effect on performance, indicating that further improvements may require more substantial changes.

Map View

Frame analysis view ee6de4ca-c099-4140-959b-398def6722ff The updated analysis of frame 2571 shows significant improvements in performance due to the applied optimizations, including the use of RepaintBoundary, reducing widget nesting, and precaching images. Build time has decreased to 23.7ms, and raster time is now 48.7ms, a substantial reduction compared to the earlier analysis where rasterization exceeded 200ms. The number of rebuilds for critical widgets like ListTile, Image, and Text has also dropped to 2 per frame, compared to 10 before, reflecting the effectiveness of simplifying widget hierarchies and isolating repaint operations. While Canvas.saveLayer() is still invoked, its impact has been mitigated, with the painting phase now being the primary contributor at 79.7ms. These results demonstrate that the optimizations have successfully reduced both UI and raster jank, improving the app’s responsiveness. Further improvements can focus on optimizing paint operations and exploring lazy loading for larger lists to enhance performance even further.

After the whole optimizations on the map feature,

Eventual connectivity strategies

We've implemented connectivity strategies to manage various scenarios where network availability may be intermittent or lost. The primary goal is to ensure that core functionalities remain accessible and usable, even in offline conditions, by leveraging local storage and reactive network monitoring.

Flutter & Swift


In both the Flutter and Swift versions of the app, we implemented the same eventual connectivity strategies to address various scenarios where network availability may be intermittent or lost. The goal is to provide a robust and seamless user experience by managing connectivity fluctuations effectively. Here are the key connectivity scenarios and design solutions for the app:

ID Connectivity Scenario 1
Event Description Users attempt to access the scoreboard feature to view the top users for the first time. If they are offline, a pop-up message informs them that an internet connection is required for the initial data fetch. However, if they are online during the initial access, the app fetches the top users data from Firebase, caches it locally, and allows users to view the leaderboard offline in future sessions.
System Response - When Offline (First Access): A pop-up is displayed, notifying users that an internet connection is needed to access the Scoreboard information for the first time.
- When Online (First Access): The app fetches data from Firebase, saves the top users in cache, and displays the data on the scoreboard. For subsequent offline sessions, users can access the cached scoreboard information even without an internet connection.
Possible Antipatterns - Unclear Initial Requirement (Antipattern #7): Not informing users that connectivity is necessary for initial access may lead to confusion if they try to access the scoreboard offline for the first time.
- Excessive Connectivity Requirement (Antipattern #3): Requiring an internet connection for each access after the initial fetch instead of caching the data could frustrate users in areas with limited connectivity.
Caching + Retrieving Strategy Firebase Data Caching for Offline Availability: On the first online access, the app retrieves the top users data from Firebase, caches it locally, and automatically checks for updates when connectivity is available. The cached data allows seamless access to the scoreboard information during offline sessions.
Storage Type Local Device Cache (used for caching top users information retrieved from Firebase)
Storage Data Type Firestore documents: data format for storing top users and other details in the local cache.
ID Connectivity Scenario 2
Event Description Users attempt to update their profile information. If they are offline, a pop-up message informs them that an internet connection is required to save changes. However, if they are online during the update attempt, the app submits the changes to the server and updates the local cache for offline consistency.
System Response - When Offline: A pop-up is displayed, notifying users that an internet connection is needed to save profile changes. Changes made offline are temporarily stored locally and flagged for submission once the device is back online.
- When Online: The app submits profile updates to the server and synchronizes the updated information to the local cache for offline access.
Possible Antipatterns - Unclear Update Behavior (Antipattern #5): Not informing users that changes will not be saved without connectivity could lead to confusion.
- Data Loss (Antipattern #4): Failing to temporarily store unsaved changes made offline risks losing user inputs.
Caching + Retrieving Strategy Profile Data Synchronization for Offline Resilience: Changes made offline are queued in a temporary cache and submitted automatically once an internet connection is restored. For online updates, the server response is cached locally to ensure profile consistency during offline sessions.
Storage Type Local Temporary Cache (for offline edits), Persistent Cache (for server-synced profile data)
Storage Data Type JSON objects: used for storing user profile information locally and synchronizing with the server.
ID Connectivity Scenario 3
Event Description Users attempt to log out of the app. If they are offline, a pop-up message informs them that logging out requires an internet connection to securely update their session status on the server. If they are online, the app logs the user out and clears session data locally.
System Response - When Offline: A pop-up is displayed, notifying users that an internet connection is needed to log out securely.
- When Online: The app updates the session status on the server, clears the local session data, and redirects the user to the login screen.
Possible Antipatterns - Insecure Logout (Antipattern #2): Allowing offline logout without server synchronization could lead to inconsistent session states.
- ** Lack of Feedback (Antipattern #6):** Failing to clearly notify users about the need for connectivity during logout may cause frustration.
Caching + Retrieving Strategy Secure Session Management: During online sessions, logout requests synchronize with the server, ensuring session consistency. Offline logout attempts are disallowed with clear messaging to maintain secure session states.
Storage Type Local Session Data (cleared during logout)
Storage Data Type Key-value pairs: used for storing session tokens and user login states locally.

Profile view

The connectivity handling in the Profile View is designed to achieve the following:

  • Real-Time Monitoring: Detect changes in internet connectivity as they happen.
  • Safe Actions: Prevent critical actions like saving profile updates or logging out if no internet connection is available.
  • User Feedback: Display clear messages to inform users when connectivity issues occur.

Pup up notification

Profile View ECNs

When the profile screen initializes, the app checks the current internet connection state using the Connectivity instance. This initial state is stored in a boolean variable, _isConnected, to determine whether the device is online. Additionally, a listener is set up to continuously monitor connectivity changes while the view is active.

The _updateConnectionStatus method is called whenever the connectivity state changes. It updates the _isConnected variable and ensures that the UI reflects the current connection status. For instance, buttons for saving the profile or logging out are disabled if the device is offline.

When users attempt to perform actions like saving their profile or logging out without an active connection, the app displays a SnackBar message notifying them of the issue. This ensures a smooth user experience by clearly explaining why the action cannot be completed.

History view

83f12a4d-98e5-483e-9ed5-68c1f684b93b

For the eventual connectivity in the history view, the corresponding connection was checked and if there is no connection the shown image lets the user know there is no connection, but from the local data, the days of the month where the user has recycled should be shown.

Local storage strategy(ies)

SQLite for storing history data was the approach used to ensure seamless functionality and data availability in mobile applications. This strategy combines local storage for offline access with remote synchronization using Firestore, providing a resilient and user-friendly experience. By structuring the local SQLite database with a table specifically for history data (e.g., with columns like id for unique identification and date for recording timestamps), the app ensures quick, queryable access to user data, even when network connectivity is unavailable. Firestore acts as the single source of truth, syncing the latest updates to the SQLite database when the app is online, thereby maintaining data consistency across sessions. This approach not only enhances performance by reducing network dependency but also supports robust error handling, enabling users to interact with their history data reliably in all conditions.

The database was initialized as:

Future<Database> _initDatabase() async {
    final databasesPath = await getDatabasesPath();
    final dbPath = join(databasesPath, 'app_data.db');

    return await openDatabase(
      dbPath,
      version: 1,
      onCreate: (db, version) {
        db.execute('''
        CREATE TABLE history (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          date TEXT NOT NULL
        )
      ''');
      },
    );
  }

Multi-threading strategy(ies)

In Flutter, the main thread, also called the UI thread, is responsible for rendering the interface and handling user interactions. Long-running or blocking tasks on this thread can cause the app to freeze or become unresponsive. To avoid such issues, Flutter relies on the Dart async and await keywords, along with isolates (Dart's threading model), to perform asynchronous operations in the background without blocking the UI.

In other words, multithreading is implicitly achieved using Dart's Future, async, and await constructs. These enable the execution of asynchronous tasks, such as camera initialization, capturing photos, and updating Firestore, without blocking the UI thread. Here's how multithreading is utilized in the app:

Future<void> _addPointsToFirestore(int newPoints) async {
  final userDoc = FirebaseFirestore.instance.collection('users').doc(userEmail);

  final docSnapshot = await userDoc.get(); // Async read from Firestore

  if (docSnapshot.exists) {
    final data = docSnapshot.data() as Map<String, dynamic>;
    final history = List<Map<String, dynamic>>.from(data['points']['history'] ?? []);
    final totalPoints = data['points']['total'] ?? 0;

    // Updating Firestore
    await userDoc.update({
      'points.history': history,
      'points.total': totalPoints + newPoints,
    });
  } else {
    // Creating a new Firestore document
    await userDoc.set({
      'points': {
        'history': [
          {'date': DateTime.now().toIso8601String().split('T')[0], 'points': newPoints},
        ],
        'total': newPoints,
      },
    });
  }
}
  • Firestore read (get) and write (update, set) operations are inherently asynchronous due to the network latency involved.
  • These operations are performed in the background using async and await, ensuring the UI remains responsive even during database updates.
Future<void> _checkInternetConnection() async {
  var connectivityResult = await Connectivity().checkConnectivity();
  setState(() {
    _isConnected = connectivityResult == ConnectivityResult.mobile ||
        connectivityResult == ConnectivityResult.wifi;
  });
}
  • Checking the internet connection involves querying the device's current network state, which can be slow.
  • By making this call asynchronous with await, the app doesn't block the main thread while waiting for the result.
  • The connectivity state is updated in the background, and the UI is refreshed with setState once the result is available.

Multiple operations, such as checking connectivity, initializing the camera, and updating Firestore, can occur simultaneously without interfering with each other.

Caching strategy(ies)

To achieve eventual connectivity in Flutter we decided to implement a caching strategy in the scoreboard feature, this to improve performance and user experience.

This specifically helps with the loading times and the offline access, as the scoreboard is fetching data from Firebase but this data is not constantly being updated, the app will have in cache the information if it has loaded before. This is specifically important when connectivity is inconsistent, for offline-first experience.

In other cases it is expected to reduce network usage and Battery efficiency by avoiding the constant fetching, as accessing the server takes up high network usage which can impact performance and cause delays in mobile environments.

Expected Behavior: Once the user access the scoreboard feature the app fetches all the information to be displayed in the list view.

Behavior with eventual connectivity: When the user access for the first time to the scoreboard and has no connection to internet a pop-up will be displayed saying that should be connected to access for the first time when data should be fetched from Firebase. If the user access this feature for the first time with connectivity, all the top users information will be stored in cache, for when the user access offline he can still get the top users.

Future<void> _loadUsers() async {
    try {
      var querySnapshot = await FirebaseFirestore.instance
                  .collection('users')
                  .orderBy('points.total', descending: true)
                  .get(GetOptions(source: Source.cache));
      print("cargando usuario desde cache");
      if (_isConnected)
      {
        querySnapshot = await FirebaseFirestore.instance
                  .collection('users')
                  .orderBy('points.total', descending: true)
                  .get(GetOptions(source: Source.server));
        print("cargando usuario desde server");
      }
     // ... resto del codigo ...

Swift

Local storage strategy(ies)

Our iOS app utilizes KeychainService for secure local storage of user data. This allows the app to access user credentials such as email, nickname, and profile picture, even when offline or not actively authenticated.

struct KeychainService {
    static let keychain = Keychain(service: "com.yourapp.SustainU")

    static func saveProfile(_ profile: UserProfile) {
        do {
            let data = try JSONEncoder().encode(profile)
            keychain["profile"] = data.base64EncodedString()
        } catch {
            print("Error saving profile: \(error)")
        }
    }

    static func loadProfile() -> UserProfile? {
        do {
            if let profileString = keychain["profile"],
               let data = Data(base64Encoded: profileString) {
                return try JSONDecoder().decode(UserProfile.self, from: data)
            }
        } catch {
            print("Error loading profile: \(error)")
        }
        return nil
    }
}

Multi-threading strategy(ies)

Our iOS app uses Grand Central Dispatch (GCD) and OperationQueues to handle asynchronous operations without blocking the main UI thread. This is particularly important for operations like image processing, network requests, and database updates. For example, in the request service:

class RequestService {
    private let imageProcessingQueue = DispatchQueue(label: "com.app.imageProcessing", 
                                                   qos: .userInitiated)
    
    func sendRequest(prompt: String, photoBase64: String, completion: @escaping (String?) -> Void) {
        imageProcessingQueue.async {
            // Heavy image processing work...
            
            DispatchQueue.main.async {
                // UI updates
                completion(result)
            }
        }
    }
}

Caching strategy(ies)

To achieve eventual connectivity in Swift we decided to implement a caching strategy in the scoreboard feature, this to improve performance and user experience.

This specifically helps with the loading times and the offline access, as the scoreboard is fetching data from Firebase but this data is not constantly being updated, the app will have in cache the information if it has loaded before. This is specifically important when connectivity is inconsistent, for offline-first experience.

In other cases it is expected to reduce network usage and Battery efficiency by avoiding the constant fetching, as accessing the server takes up high network usage which can impact performance and cause delays in mobile environments.

Expected Behavior: Once the user access the scoreboard feature the app fetches all the information to be displayed in the list view.

Behavior with eventual connectivity: When the user access for the first time to the scoreboard and has no connection to internet a pop-up will be displayed saying that should be connected to access for the first time when data should be fetched from Firebase. If the user access this feature for the first time with connectivity, all the top users information will be stored in cache, for when the user access offline he can still get the top users.

Ethics video

https://youtu.be/7Tkk10v60ZU

⚠️ **GitHub.com Fallback** ⚠️