Sprint 2 - ISIS-3510-DreamTeam/Sprint1-DesignThinking GitHub Wiki
Sprint 2 focused on the implementation of BreakLoop, transforming the concepts and designs developed during Sprint 1 into a functional mobile application. During this sprint, the team implemented the application's core functionalities using Flutter and Kotlin and developed the analysis pipeline required to collect, process, and analyze user data. The implementation was guided by the business questions and the functional and architectural requirements established for the application.
The implemented solution includes features related to phone sensors, user authentication, Type 2 business questions, context aware interactions, smart features, and external services. Additionally, the team implemented the application views, the proposed architecture, architectural and design patterns, and the analysis components needed to support the implemented business questions. Each team member made specific contributions to the functionality, business questions, architecture, design patterns, and application views.
| Member | Platform |
|---|---|
| Sara González | Kotlin |
| Juan Felipe Camelo | Kotlin |
| Valentina Jiménez | Kotlin |
| Hernán Ramírez | Flutter |
| Pablo Ramírez | Flutter |
| Laura Muñoz | Flutter |
| Member | Platform | Architectural pattern | Design patterns | Views | Functionalities |
|---|---|---|---|---|---|
| Sara González | Kotlin | MVVM | Strategy Observer |
Progress & Statistics View Offline Activities View Activity Detail View |
Context-Aware Functionality Type 2 BQ: Screen-Time Reduction Progress (BQ3) |
| Juan Felipe Camelo | Kotlin | MVVM | State Repository |
Login, Sign-up, Recover Password, Home (V1) | Account Authentication; Daily Screen-Time Goal |
| Valentina Jiménez | Kotlin | MVVM | Repository Adapter |
Focus View Detailed Statistics View |
Backend / External Service Connection Phone-checking behavior detection |
| Hernán Ramírez | Flutter | MVVM | State Repository |
Login, Sign-up, Recover Password, Home | User Authentication, Smart-feature (personalized objectives) |
| Pablo Ramírez | Flutter | MVVM | Observer Repository |
Progress & Statistics View Offline Activities View Activity Detail View |
BQ2 Screen-Time Reduction Progress Context-Aware Functionality |
| Laura Muñoz | Flutter | MVVM | Adapter Repository |
Detailed Statistics View Focus View |
Phone-checking behavior detection Backend / External Service Connection |
| Responsible | Platform | Business Question | Type | Rationale |
|---|---|---|---|---|
| Sara González | Kotlin | Which categories of suggested offline activities have the highest acceptance rate among users? | Type 4 | This BQ uses the activity logs produced by the context-aware recommendations. Knowing which categories (Move, Create, Relax, Socialize) users accept most often reveals aggregated preferences that can improve the recommender weights and, in a privacy-preserving form, could be valuable to organizations that offer related offline activities, supporting the partnership revenue model from Sprint 1. |
| Juan Felipe Camelo | Kotlin | How much remaining screen time does the user have available today, based on their defined daily goal? | Type 2 | It gives the user an immediate, clear and visible number on the Home screen: how much of today's allowance is used and how much is left. It is the direct, per-day complement of BQ3, which measures the weekly reduction. |
| Valentina Jiménez | Kotlin | How much does Focus Mode help users maintain uninterrupted study or work sessions? | Type 2 | Focus Mode helps users maintain uninterrupted study or work sessions by providing a dedicated focus timer that encourages users to stay in a session without switching to distracting activities. Its effectiveness can be evaluated by measuring the percentage of Focus sessions completed without interruption, the average duration of uninterrupted sessions, and the number of sessions abandoned before completion. |
| Hernán Ramírez | Flutter | Do users who accept offline activity suggestions maintain longer streaks of meeting their screen-time goals than users who do not accept them? | Type* | This BQ was selected because it connects the personalized daily goal with the offline activity recommendations. The goal feature keeps a streak of consecutive days in which the user stays under their screen-time limit, and the activity logs record which suggestions the user accepted and completed. Comparing the streaks of users who accept suggestions with those who do not shows whether offering screen-free alternatives actually helps users keep their goals over time. Since it combines behavioral outcomes (streaks) with feature usage (accepted activities), it is a cross-type question rather than one focused on a single aspect of the app. |
| Pablo Ramírez | Flutter | How much does users' weekly screen time decrease after using the app's interventions? | Type 2 | This BQ was selected because it directly measures the main goal of BreakLoop, which is helping users reduce their screen time. The answer is delivered to the user in the Progress & Statistics view, which compares their current weekly average with their first week of use and shows the change as a percentage. Instead of only showing how much they used their phone, this gives users clear feedback on whether their habits are improving, which helps them stay motivated and decide whether to keep or adjust their behavior. |
| Laura Muñoz | Flutter | What percentage of accelerometer events can be successfully captured and processed by the application to support phone pickup detection? | Type 1 | This BQ was selected to evaluate the reliability of the accelerometer data collection implemented through the sensors_plus library. By measuring the percentage of accelerometer events that are successfully captured and processed by the application, we can assess whether the sensor integration provides consistent data to support phone pickup detection and identify potential limitations in the feature's performance across devices. |
| BQ | Change | Reason |
|---|---|---|
| BQ15 (new) | Added a new Type 2 question: "How much remaining screen time does the user have available today, based on their defined daily goal?" | The Sprint 1 list had no question about the daily goal, although View 1 (Home) already showed a screen-time card with "% vs. goal" and "remaining daily allowance". The new question makes that card answer a stated BQ. |
The application includes several functionalities that address different user needs and demonstrate the use of sensors, contextual information, intelligent features, authentication, and external services. The main functionalities developed by the team are summarized below.
| Functionality | Description |
|---|---|
| Sensor | Uses device sensors to collect information and support application functionality. |
| Type 2 BQ | Implements the required Type 2 BQ functionality defined for the project. |
| Context-aware | Adapts the application behavior based on contextual information about the user or device. |
| Smart Feature | Provides intelligent or automated behavior based on collected information or predefined conditions. |
| Authentication | Allows users to authenticate and access the application securely. |
| External Service / Backend | Stores Focus Session data locally and synchronizes unsynced sessions with the Firebase Firestore backend. |
- Functionality: Phone-checking behavior detection
- Sensor used: Accelerometer
- Views: Detailed Statistics View
- Responsible: Valentina Jiménez / Laura Muñoz
Description:
The functionality uses the phone's accelerometer to detect phone pickups based on changes in acceleration. When a pickup is detected, its timestamp is stored in the repository. The collected data is then processed to calculate daily phone pickups, the average time between phone checks, and the percentage of mindful and impulsive pickups. These results are displayed in the Detailed Statistics
Justification:
This functionality helps users understand their phone-checking behavior beyond total screen time. By showing the number of daily pickups, the average interval between checks, and the estimated intentionality of pickups, BreakLoop provides users with information that can help them become more aware of their habits and make more intentional decisions about checking their phone.
| Kotlin | Flutter |
|---|---|
|
|
- Functionality: BQ2 Screen-Time Reduction Progress
- Views: Progress & Statistics View
- Responsible: Sara González / Pablo Ramírez
Description:
This functionality addresses a Type 2 Business Question by showing users how much their weekly screen time has changed compared with their first week of use. The application uses the user's daily phone usage to calculate the average screen time of each week and compares the current week against that first week. The results are presented in the Progress & Statistics view, which displays the current weekly average, a chart with the screen time of each day, and a clear message such as "X% less than your first week". If the user has not allowed access to their usage data, the view explains why it is needed and guides them to enable it.
Justification:
This functionality helps users know whether their screen time is really going down, instead of only seeing the time of a single day. By providing a clear comparison against their own starting point and a day-by-day view, BreakLoop allows users to become aware of their progress, notice which days increase their usage, and decide whether to keep or adjust their habits. It directly supports the main goal of the app, which is helping students reduce their screen time and maintain that change over time.
| Kotlin | Flutter |
|---|---|
|
|
- Functionality: Context-Aware Offline Activity Recommendations
- Contextual information used: time of day, weekday/weekend, available time selected by the user, current weather (approximate location), user interests, and recently completed activities
- Views: Offline Activities View, Activity Detail View
- Responsible: Sara González / Pablo Ramírez
Description: At the top of the Offline view, a "Suggested for right now" card recommends the single best offline activity for the user's current situation. The user selects how much time they have (5, 15 or 30 minutes), and BreakLoop builds a snapshot of the current context: time of day and day type from the device clock, current weather from the Open-Meteo API using the phone's approximate location, the interests the user selected, and the activities they completed recently.
Each activity in the catalog is first filtered by hard rules: activities longer than the available time are excluded, and outdoor activities are excluded when it is raining, snowing, or at night. The remaining activities are scored by a set of weighted rules (interest match, weather fit, time-of-day fit, how well the duration uses the available time, and a penalty for repeating a recent activity). The highest-scoring activity is shown together with the reason it was chosen (for example, "Matches your interests · Fits your 15 min").
Tapping the suggestion or any activity in the list opens the Activity Detail view, where the user can start a timer and log the activity. Each started and completed activity is stored with the context in which it happened, and recent activities feed back into the next recommendation.
Justification: Interviews identified boredom and unstructured time as key triggers for excessive phone use. A generic list of alternatives is easy to ignore, while a suggestion that fits the exact moment (no outdoor walk when it rains, nothing longer than the time available) is more likely to be accepted. This allows BreakLoop to address not only the behavior of reaching for the phone but also what users can do instead, which is the core of the value proposition.
Quality scenarios covered:
- Q1: a suggestion appears in under 3 seconds (weather lookup is capped at 2.5 s) and can be started in 2 taps.
- Q2: without internet, suggestions still come from the catalog bundled in the app, with an "Offline-based" label and no error.
- Q8: a new user with no history or interests still receives suggestions, because rules without data contribute a neutral score.
Known limitations: the interest-selection screen and syncing activity logs to Firestore are planned for the next sprint. If the user leaves the detail view while the timer runs, the session is not recovered.
| Kotlin | Flutter |
|---|---|
|
|
- Functionality: Daily Screen-Time Goal
- Business Question: BQ15 – How much remaining screen time does the user have available today, based on their defined daily goal?
- Responsible: Juan Felipe Camelo / Hernán Ramírez
- Backlog IDs: STA-01, GOAL-04 (manual goal only), GOAL-05 (current streak only), SET-04
Description: The Home screen shows a card with today's screen time. The user taps the gear icon on the card to set a personal daily goal in hours and minutes. Once a goal exists, the card shows the percentage of the goal already used and the time remaining, and changes color with the situation: green while under 80 % of the goal, orange from 80 % to 100 %, and red once the goal is exceeded. If no goal is set, the card invites the user to set one.
Two related behaviors use the same calculation:
- Goal-exceeded notification. When today's screen time passes the goal, the phone posts one notification saying how far over the goal the user is, even if BreakLoop is closed. A background task checks this about every 15 minutes and when the app opens. The notification is sent at most once per day, and again if the user changes the goal.
- Streak. The top bar shows how many consecutive completed days ended at or under the goal. It drops to 0 when today exceeds the goal.
Justification: The persona sets screen-time limits but extends them once she reaches the cap. Showing the remaining allowance before the limit is reached, and warning at 80 %, gives her the information while she can still act on it. The notification covers the case where she is in another app and never opens BreakLoop.
Evidence:
Kotlin:
| On track | Near the limit | Goal exceeded |
|---|---|---|
![]() |
![]() |
![]() |
| Goal configuration | Notification |
|---|---|
![]() |
![]() |
Flutter:
| Near the limit | Goal exceeded |
|---|---|
![]() |
![]() |
| Goal configuration |
|---|
![]() |
- Functionality: User Authentication
- Views: Login / Authentication View
- Responsible: Juan Felipe Camelo / Hernán Ramírez
Description:
This functionality allows users to [create an account / log in / log out / etc.]. The application [briefly explain how authentication works, e.g., validates credentials and maintains the user's session].
Justification:
Authentication allows the application to identify each user and provide a personalized experience. It also helps protect user-specific information and ensures that features associated with the user's account can be accessed appropriately.
| Kotlin | Flutter |
|---|---|
|
|
| Kotlin | Flutter |
|---|---|
|
|
| Kotlin | Flutter |
|---|---|
|
|
| Kotlin | Flutter |
|---|---|
|
|
- Functionality: Focus Session Data Storage and Synchronization
- External service / backend: pring Boot Backend deployed on Google Cloud Run + Firebase Firestore
- Views: Focus Session View
- Responsible: Valentina Jiménez / Laura Muñoz
Description:
This functionality connects the Android application with the BreakLoop backend deployed on Google Cloud Run. The Focus View sends completed or interrupted focus sessions to the backend, which stores the session information in Firebase Firestore. The application communicates with the backend through REST endpoints to save and retrieve focus session data, including the session duration, type, status, and XP earned.
Justification:
Connecting the application to the backend allows focus session data to be persisted beyond the local device and associated with the user's account. This supports the Focus Mode functionality by enabling the application to store and retrieve session history, providing a foundation for tracking users' focus behavior and generating statistics over time.
| Kotlin | Flutter | Cloud Server |
|---|---|---|
![]() |
![]()
|
![]() |
![]() |
![]() |
Description:
Rationale:
| Data Source | Platform(s) | Data Collected | Purpose (BQ) |
|---|---|---|---|
| Application Usage | Kotlin | Per-app foreground time and daily screen-time totals, derived from Android usage events; stored locally in Room (app_usage, daily_usage_stats) |
Screen-time reduction progress (BQ3) |
| Offline Activities | Kotlin | Started and completed activities with source (recommended or browsed), XP, time of day, weather and available time; stored locally in Room (activity_logs) |
Recommender history and BQ11 |
| Phone Sensor | [Platform] | [Data] | [Purpose / BQ#] |
| User Interactions (events) | [Platform] | [Data] | [Purpose / BQ#] | | Device Telemetry | [Platform] | [Data] | [Purpose / BQ#] | | [Source] | [Platform] | [Data] | [Purpose / BQ#] |
[Describe the transformations, calculations, aggregations, or preprocessing applied to the collected data, and where each step runs (on device, backend, or warehouse).]
- Technology: [Database / data warehouse / storage]
Description: [Explain what data is stored and how it is organized (collections, tables, partitions).]
Rationale: [Explain why this storage was chosen.]
| BQ | Platform | Responsible | Data Used | Analysis | Result / Visualization |
|---|---|---|---|---|---|
| BQ11 | Kotlin | Sara González |
activity_logs (activityId, source, completedAt), joined with the activity catalog to obtain each category |
For each category, acceptance = activities started from a suggestion (source = RECOMMENDED) and completed, divided by all recommended activities started in that category. Categories are ranked by this rate. | [Result / link] |
| BQ15 | Kotlin | Juan Felipe Camelo | Today's screen time (daily_usage_stats.screenTimeMs, local Room database) and the user's daily goal (daily_goal_ms, DataStore) |
Remaining = goal − used; percentage = used ÷ goal; state: not set, on track (< 80 %), near limit (80–100 %), exceeded (> 100 %) | Home card with time used, % of goal and time left; notification when exceeded; streak badge in the top bar |
| [BQ#] | Kotlin | Valentina Jimenez | focusSessions (duration, status, startTime, type, xpEarned) stored in Firebase Firestore | Calculates total focus sessions, completed sessions (COMPLETED), abandoned sessions (ABANDONED), completion percentage = completed sessions ÷ total sessions × 100, and average session duration. | Analytics endpoint /api/analytics/focus returning the calculated metrics; can be visualized as completion percentage, completed vs. abandoned sessions, and average duration. |
| BQ5 | Flutter | Laura Muñoz |
focus_sessions (durationSeconds, sessionStatus, startedAt, sessionType, earnedXp) stored locally and synchronized with Firebase Firestore |
Calculates the number of completed and abandoned sessions, completion percentage, average session duration, and total XP earned to show users their Pomodoro session consistency. | Focus Session statistics showing completed vs. abandoned sessions, completion percentage, average duration, and XP earned. |
| BQ13 | Flutter | Hernán Ramírez | currentStreak, lastEvaluationDate, streakBrokenToday (via GoalsRepository, shared_preferences) cross-referenced with offline activity suggestion acceptance events | Compares the streak length (consecutive days meeting the screen-time goal) between users who accept offline activity suggestions and users who do not, to assess whether engaging with offline recommendations correlates with longer streaks. | Comparison of average streak length between the two user groups, segmented by acceptance rate of offline activity suggestions |
| [BQ#] | Flutter | [Name] | [Data] | [Analysis] | [Result / link] |
Description:
The diagram shows the two BreakLoop clients, the native Android app (Kotlin) and the cross-platform app (Flutter), and the components they share. Each client stores data locally (Room / SharedPreferences) and reads usage, sensor and location data from the Android system APIs; the Flutter app does it through a platform channel, sensors_plus and geolocator. Both clients authenticate with Firebase Authentication, get the weather from Open-Meteo, read and write user data in Cloud Firestore, send events to Firebase Analytics, and call the Spring Boot Backend API deployed on Cloud Run through REST / JSON. The backend accesses Cloud Firestore with the Firebase Admin SDK. For analytics, Firestore data is exported on a schedule and Firebase Analytics events are exported to a separate Analytics Store. Each connection is drawn as an interface labeled with the protocol or SDK it uses.
Rationale:
Both clients connect to the same set of components so that the two apps behave the same way, share one data schema, and produce comparable data for the business questions. Local storage comes first in each client so the app keeps working without connection and syncs when it is back online. Firebase was chosen for authentication, storage and event collection because it removes the need to manage our own infrastructure, while the Spring Boot backend on Cloud Run centralizes the business logic that should not live in the clients and accesses Firestore with administrator privileges. Open-Meteo is called directly by the clients because it is free, needs no API key, and the suggestion can still be shown if the weather is not available. Finally, analytics data is moved to a separate store so that the queries that answer the business questions do not affect the database used by the app.
Description:
The class diagram shows the BreakLoop domain model, centered on the User class. The classes are grouped into the main areas of the app:
-
Usage tracking:
DistractingApp,DailyStats,DailyAppUsageandPhoneCheck. -
Goals:
Goal. -
Offline activities:
OfflineActivity,ActivityLogandContextSnapshot. -
Focus and interventions:
FocusSession,PriorityContact, the abstractInterventionclass with its subclasses,WindDownScheduleandChallengeGate. -
Gamification:
Pet. -
Social:
Friendship,QuestMembershipandQuest.
Fixed sets of values, such as statuses, types and categories, are modeled as enumerations. Relationships use multiplicities, compositions for owned data, generalization for the interventions, and a dependency between DailyStats and PhoneCheck.
Rationale:
User is the center of the model because every piece of data in BreakLoop belongs to a user, which also matches how the data is organized in Firestore. Compositions are used when the data has no meaning without its owner (for example, a user's daily statistics or the context saved with an activity log), so it is created and deleted together with it. The offline activity catalog is separated from the activity logs: the catalog is shared by all users, while each log stores what a specific user did and in which context, which is the data the recommender and several business questions rely on. Intervention is an abstract class so new types of interventions can be added without changing FocusSession or WindDownSchedule. Many-to-many relationships that need their own data, such as friendships and quest participation, are modeled as separate classes. Enumerations keep the same values across both apps and the database, and the multiplicities reflect the app rules; for example, a flagged app can trigger many challenge gates so their history can be analyzed. The same model is used by the Kotlin and Flutter apps so both store and read data in the same way.
- Responsible: Valentina Jiménez
Description: The deployment architecture shows the main components of BreakLoop and how they are distributed across the Android device, cloud services, and Firebase. The Android application contains the User, Focus, Usage, Recommendation, and Statistics modules, together with local persistence. The application communicates through HTTP/REST with the Spring Boot backend deployed on Google Cloud Run and with the Analytics Service. The Spring Boot backend and Analytics Service access Firebase, which provides Authentication, Firestore persistence, and analytics-related data storage. The Analytics Service retrieves and processes data stored in Firestore to calculate the metrics required by the Business Questions.
Rationale: The deployment separates mobile application functionality from backend and analytics responsibilities. The Android device handles user interaction, feature execution, sensor processing, and local persistence, while Google Cloud Run provides scalable deployment for the backend services. Firebase is used as the cloud persistence and authentication layer, allowing user data and application records to persist beyond the device. The Analytics Service is deployed as a separate backend component so that analytical processing and BQ calculations remain independent from the mobile application and can access centralized Firestore data without adding analytics logic directly to the Android modules.
The BreakLoop application follows the Model-View-ViewModel (MVVM) architectural pattern. This pattern was applied to both the Kotlin and Flutter implementations to provide a consistent structure for the application's functionalities and to separate the user interface from the application logic and data management.
The architecture is divided into three main components. The View is responsible for displaying the user interface and handling user interactions. The ViewModel acts as an intermediary between the View and the Model, managing the application state and the logic required to process user actions and update the interface. The Model is responsible for handling the application's data and the operations required to access or modify it.
In BreakLoop, each team member contributed to the implementation of this architecture through the development of their assigned functionalities and views. Although the specific components and classes vary between the Kotlin and Flutter implementations, both follow the same general separation between presentation, application logic, and data.
Implementation: The View interacts with its corresponding ViewModel rather than directly handling the application's main logic. The ViewModel processes the required operations and communicates with the data or Model layer when information needs to be retrieved, stored, or processed. This structure was used for functionalities such as user authentication, phone-checking behavior detection, screen-time reduction progress, context-aware features, and smart features.
Responsibility: The main responsibility of MVVM in BreakLoop is to separate the presentation logic from the user interface and data management. This allows changes to the interface to be made without modifying the underlying application logic and makes each functionality easier to organize and maintain.
Rationale: MVVM was selected because BreakLoop contains several independent functionalities and views that need to interact with user and device data. Separating these responsibilities helps reduce the coupling between the interface and the underlying logic, making the application easier to maintain and extend. Additionally, applying the same architectural approach to both Kotlin and Flutter provides a consistent structure across the two implementations.
| Pattern | Platform | Where it is used | Responsible |
|---|---|---|---|
| Strategy | Kotlin |
RecommendActivityUseCase and the ScoringRule implementations (domain/recommendation/) |
Sara González |
| Observer | Kotlin |
StatsViewModel (Room Flow → combine → StateFlow) and StatsScreen
|
Sara González |
| State | Kotlin |
GoalProgress, CalculateGoalProgressUseCase, ScreenTimeGoalCard, GoalExceededChecker
|
Juan Felipe Camelo |
| Observer | Flutter |
ScreenTimeProgressViewModel (ChangeNotifier) and ScreenTimeProgressPage (context.watch, WidgetsBindingObserver) (screen_time_progress/) |
Pablo Ramírez |
| Repository | Flutter |
ScreenTimeRepository and UsageStatsScreenTimeRepository (screen_time_progress/data/) |
Pablo Ramírez |
| Adapter | Flutter |
SensorsPlusAccelerometerAdapter and IAccelerometerService (phone_check/) |
Laura Muñoz |
| Repository | Flutter |
FocusSessionRepository and PhoneCheckRepository (data/) |
Laura Muñoz |
| Dependency Injection | Kotlin / Java | FirebaseConfig, where FirebaseApp and Firestore are provided as Spring beans and injected into controllers/services | Valentina Jimenez |
| Repository | Kotlin | FocusSessionRepository, which abstracts local persistence of Focus sessions using RoomFocusSessionRepository, which abstracts local persistence of Focus sessions using Room | Valentina Jimenez |
| Adapter / API abstraction | Kotlin | FocusApi, which abstracts communication between the Android application and the Spring Boot backend through Retrofit | Valentina Jimenez |
| State | Flutter | PasswordResetView and GoalsStatusView, in both the visual component is dependent on some state | Hernán Ramírez |
| Repository | Flutter | AuthRepository centralizes and exposes the firebase-auth functionalities | Hernán Ramírez |
- Pattern: Strategy
- Responsible: Sara González
Problem addressed: The recommender has to combine several independent criteria (interests, weather, time of day, duration, repetition). Hard-coding them inside one function would make it difficult to add, remove or re-weight a criterion, and both apps must use exactly the same rules.
Implementation: Each criterion is a class that implements the ScoringRule interface, exposing its weight, the reason it contributes, and a score between 0 and 1. RecommendActivityUseCase receives the list of rules through its constructor and simply adds up weight × score for each candidate, without knowing which rules exist. The default set lives in DefaultScoringRules.
interface ScoringRule {
val weight: Double
val reason: ReasonTag?
fun score(activity: OfflineActivity, context: ContextSnapshot): Double
}
class RecommendActivityUseCase(private val rules: List<ScoringRule>) {
fun rank(catalog: List<OfflineActivity>, context: ContextSnapshot): List<Recommendation> {
// ...hard filters...
val score = rules.sumOf { it.weight * it.score(candidate, context) }
val reasons = rules.filter { it.score(candidate, context) == 1.0 }.mapNotNull { it.reason }
// ...
}
}Rationale: Strategy makes each rule small and testable in isolation, and new context signals (for example, location type) can be added as a new class without modifying the recommender. Negative weights turn a rule into a penalty without special cases, and keeping weights in one place makes it easy to align them with the Flutter implementation.
- Pattern: Observer
- Responsible: Sara González
Problem addressed: Screen-time data changes on its own: a background WorkManager job writes new totals to the database every 15 minutes. The statistics view must reflect those changes without the user refreshing manually and without the view querying the database directly.
Implementation: Room DAOs return Flows that emit whenever a table changes. StatsViewModel combines three of them (today's apps, this week and last week), builds the UI state, and publishes it through a StateFlow. StatsScreen observes that state with collectAsStateWithLifecycle and redraws automatically.
combine(
repository.observeAppUsage(todayStr),
repository.observeDailyStats(monday.toString(), sunday.toString()),
repository.observeDailyStats(lastMonday.toString(), lastSunday.toString())
) { apps, thisWeek, lastWeek ->
val summary = buildWeeklySummaryUseCase.buildSummary(thisWeek, lastWeek, monday, todayStr)
StatsUiState.Content(
totalMs = apps.sumOf { it.foregroundMs },
apps = apps.sortedByDescending { it.foregroundMs },
weeklySummary = summary,
reduction = calculateReductionUseCase.calculate(baseline, summary.weeklyAverageMs)
)
}.collect { state -> _uiState.value = state }Rationale: The chain Room → Flow → ViewModel → StateFlow → UI keeps the view always consistent with the stored data, decouples the background worker from the screen, and fits naturally with MVVM: the view only observes state, and the ViewModel is the only one that changes it.
- Pattern: State
- Responsible: Juan Felipe Camelo
Problem addressed: The Home goal card must look and behave differently depending on the situation: no goal defined, comfortably under the goal, close to the limit, or over it. Written as scattered if checks on numbers, every place that cares (the card, the notification) would repeat the thresholds and could disagree.
Implementation: The possible situations are modeled as a closed set of states. Each state holds only the data that makes sense for it.
sealed interface GoalProgress {
data object NotSet : GoalProgress
data class OnTrack(val usedMs: Long, val goalMs: Long) : GoalProgress {
val remainingMs: Long get() = goalMs - usedMs
}
data class NearLimit(val usedMs: Long, val goalMs: Long) : GoalProgress {
val remainingMs: Long get() = goalMs - usedMs
}
data class Exceeded(val usedMs: Long, val goalMs: Long) : GoalProgress {
val exceededMs: Long get() = usedMs - goalMs
}
}One use case owns the transition rules:
class CalculateGoalProgressUseCase {
fun calculate(usedMs: Long, goalMs: Long?): GoalProgress {
return when {
(goalMs == null || goalMs <= 0) -> GoalProgress.NotSet
(goalMs < usedMs) -> GoalProgress.Exceeded(usedMs, goalMs)
((goalMs * 0.8) <= usedMs) -> GoalProgress.NearLimit(usedMs, goalMs)
else -> GoalProgress.OnTrack(usedMs, goalMs)
}
}
}The components then define their behavior from the state, with no numeric checks of their own. The card chooses its message and color:
// PASTE the `when (progress) { ... }` block from ScreenTimeGoalCard.kt hereand the background checker only notifies in one state:
if ((state !is GoalProgress.Exceeded) || (goalRepository.getLastNotifiedDate() == today)) { return }
notifier.notifyGoalExceeded(state.exceededMs)Rationale: Kotlin's sealed interface makes the when exhaustive: if a new state is added, every component that renders or reacts to the goal stops compiling until it handles it. The thresholds live in one place, and each state can only expose valid data (there is no "remaining time" on an exceeded goal). The same idea is used for the screen itself: HomeUiState is Loading, NoPermission or Content.
Evidence:
- Pattern: Adapter
- Responsible: Laura Muñoz
Problem addressed:
The phone-checking behavior feature needs to receive accelerometer data from the sensors_plus library, but the application should not depend directly on the library's event format. Directly using the external API throughout the application would increase coupling and make it harder to change the sensor library in the future.
Implementation:
The application defines its own IAccelerometerService interface with the format expected by the phone-checking feature. SensorsPlusAccelerometerAdapter implements this interface and adapts the AccelerometerEvent objects provided by sensors_plus into a List<double> containing the X, Y, and Z acceleration values. PhoneCheckService then uses the application's interface instead of interacting directly with sensors_plus.
abstract class IAccelerometerService {
Stream<List<double>> get accelerationStream;
}
class SensorsPlusAccelerometerAdapter implements IAccelerometerService {
@override
Stream<List<double>> get accelerationStream {
return accelerometerEventStream(
samplingPeriod: SensorInterval.normalInterval,
).map((event) {
return [event.x, event.y, event.z];
});
}
}Rationale:
Adapter isolates the phone-checking logic from the external sensor library. This makes the service easier to maintain and test, and allows the underlying sensor implementation or library to be changed without modifying the rest of the feature. The adapter also provides a consistent data format for the application regardless of how the external library represents accelerometer events.
- Pattern: Repository
- Responsible: Laura Muñoz
Problem addressed:
The application needs to store and synchronize Focus Session data, but the ViewModel should not directly handle the details of local storage or communication with Firestore. Putting these responsibilities inside the ViewModel would couple the application logic to specific data sources.
Implementation:
FocusSessionRepository provides a single interface for managing Focus Session data. It stores sessions locally using SharedPreferences and synchronizes unsynced sessions with Firestore through FocusRemoteDatasource. The FocusViewModel only interacts with the repository through methods such as saveSessionLocally() and syncUnsyncedSessions().
class FocusSessionRepository {
Future<void> saveSessionLocally(FocusSessionModel session) async {
// Save session in local storage
}
Future<List<FocusSessionModel>> getUnsyncedSessions() async {
// Retrieve sessions pending synchronization
}
Future<void> syncUnsyncedSessions() async {
// Upload pending sessions to Firestore
}
}Rationale: Repository separates the application's session logic from data access. This reduces coupling between the FocusViewModel and the storage technologies, makes the data layer easier to maintain, and allows local and remote storage to be managed through a single abstraction. It also supports synchronization by keeping unsynced sessions locally until they can be successfully uploaded to Firestore.
- Pattern: Observer
- Responsible: Pablo Ramírez
Problem addressed:
The Progress & Statistics view must show the user's screen-time progress, but the data is loaded asynchronously and can be in different states: loading, waiting for the usage access permission, loaded, or failed. If the view had to check these changes by itself, it would be coupled to the loading logic and could show outdated information. In addition, the user leaves the app to grant the usage access permission in the Android settings, so the view must react when the user comes back.
Implementation:
ScreenTimeProgressViewModel extends ChangeNotifier and acts as the observable subject. Every time its state changes (status, weekly data, or reduction percentage), it calls notifyListeners(). ScreenTimeProgressPage subscribes to the ViewModel through context.watch (Provider) and rebuilds automatically when it is notified. The page also implements WidgetsBindingObserver to observe the app lifecycle: when the app is resumed and the permission was still missing, it asks the ViewModel to load the data again.
class ScreenTimeProgressViewModel extends ChangeNotifier {
ScreenTimeProgressStatus status = ScreenTimeProgressStatus.idle;
List<WeeklyScreenTime> weeks = [];
double? reductionPercent;
Future<void> load() async {
status = ScreenTimeProgressStatus.loading;
notifyListeners();
final records = await _repository.getDailyScreenTime();
weeks = _groupByWeek(records);
reductionPercent = _computeReduction();
status = ScreenTimeProgressStatus.loaded;
notifyListeners();
}
}
// ScreenTimeProgressPage: rebuilds every time the ViewModel notifies a change
final vm = context.watch<ScreenTimeProgressViewModel>();
// Reloads the data when the user comes back from the Android settings
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final vm = context.read<ScreenTimeProgressViewModel>();
if (state == AppLifecycleState.resumed && vm.status == ScreenTimeProgressStatus.permissionRequired) {
vm.load();
}
}Rationale:
Observer keeps the view always consistent with the ViewModel state without the view having to ask for changes. It fits naturally with MVVM: the ViewModel is the only one that changes the state, and the view only observes it and renders the corresponding screen (loading, permission request, error, or progress). Observing the app lifecycle also improves the user experience, because the progress appears automatically after granting the permission, without restarting the app or refreshing manually.
- Pattern: Repository
- Responsible: Pablo Ramírez
Problem addressed:
The Screen-Time Reduction Progress feature needs the user's daily screen time, which comes from Android's usage statistics through native code and requires a special permission. If the ViewModel accessed this source directly, it would be coupled to the Android implementation and would have to handle details such as the platform channel and the permission checks, mixing data access with the progress calculations.
Implementation:
ScreenTimeRepository defines a single interface to check and request the usage access permission and to get the daily screen-time records. UsageStatsScreenTimeRepository implements it by reading the real usage of the phone through a platform channel and converting the result into DailyScreenTime objects. The repository is created in app.dart and injected into ScreenTimeProgressViewModel through Provider, so the ViewModel only depends on the interface.
abstract interface class ScreenTimeRepository {
Future<bool> hasPermission();
Future<void> requestPermission();
Future<List<DailyScreenTime>> getDailyScreenTime();
}
class UsageStatsScreenTimeRepository implements ScreenTimeRepository {
static const _channel = MethodChannel('breakloop/usage_stats');
@override
Future<List<DailyScreenTime>> getDailyScreenTime() async {
final minutesByDay = await _channel.invokeMapMethod<String, int>('getDailyUsage');
return [
for (final entry in minutesByDay!.entries)
DailyScreenTime(date: DateTime.parse(entry.key), minutes: entry.value),
];
}
// hasPermission() and requestPermission() ...
}
// The ViewModel only knows the interface, not where the data comes from
class ScreenTimeProgressViewModel extends ChangeNotifier {
final ScreenTimeRepository _repository;
ScreenTimeProgressViewModel(this._repository);
}Rationale:
Repository separates the progress calculations from the way the data is obtained. The ViewModel only asks for the daily records and the permission status, without knowing anything about platform channels or Android APIs, which keeps it simpler and easier to maintain. It also makes the feature easier to extend: a future data source, such as screen-time data synchronized with Firestore, can be added as a new implementation of the interface without modifying the ViewModel or the view.
- Pattern: State
- Responsible: Hernán Ramírez
Problem addressed:
The UI for authentication and goal-tracking screens needs to render different content and behavior depending on what is happening internally (loading data, waiting for setup, showing an error, etc.), without scattering conditional checks across the widget tree.
Implementation:
Rationale:
Representing the ViewModel's condition as an explicit enum makes every possible state visible and exhaustive at compile time (the switch won't compile if a case is missing), instead of relying on combinations of booleans that could produce invalid or ambiguous states. The same pattern is used in PasswordResetViewModel (ResetStatus), keeping state handling consistent across the app.
- Pattern: Repository
- Responsible: Hernán Ramírez
Problem addressed:
ViewModels need to read and persist data (user credentials, goal settings, screen-time usage) without being coupled to the specific data source (Firebase Auth, local device storage, Android's usage-stats API), so that the source can be tested, mocked, or swapped without changing business logic.
Implementation:
Rationale:
Each repository (AuthRepository, GoalsRepository) hides its underlying data source behind a small set of methods and a stream for reactive updates. This isolates the rest of the app from specific APIs (Firebase, shared_preferences). This allows that a future change, such as moving goal storage from local device persistence to cloud sync, only requires modifying GoalsRepository internally, with no changes to GoalsViewModel or any view.
- Tactic: Bounded response time (timeouts) and caching
- Quality scenario(s): Q1, Q10
- Platform(s): Kotlin
- Responsible: Sara González
Implementation: The context-aware recommendation caps the location lookup at 2 s, the weather API at 1.5 s, and the whole weather step at 2.5 s; if any is exceeded, the suggestion is still shown without weather. Weather results are cached for 30 minutes, and the activity catalog is read once and kept in memory. Screen-time tracking uses a periodic WorkManager job instead of a continuously running service, and reads Android's recorded events instead of measuring time actively.
Rationale: Q1 requires a suggestion in under 3 seconds regardless of network conditions, and Q10 requires tracking with minimal battery and CPU impact.
- Tactic: [Tactic]
- Quality scenario(s): [Q#]
- Platform(s): [Platform]
- Responsible: [Name(s)]
Implementation: [Implementation]
Rationale: [Rationale]
- Tactic: Authenticate Actors / Limit Access
- Quality scenario(s): Specific Quality Scenarios for this attribute were not defined previously. Some are provided below.
- Platform(s): Flutter
- Responsible: Hernán Ramírez
Implementation: Authentication uses Firebase Auth (email/password), isolated behind an AuthRepository so no other code touches the SDK directly. Session tokens persist locally, and a centralized go_router redirect guard blocks access to protected routes without an active session, sending unauthenticated users to login, while authenticated users are allowed further from auth screens. Password reset uses Firebase's built-in email enumeration protection, and sign-in is protected by Play Integrity/reCAPTCHA tied to the app's signing certificate.
Rationale: Delegating credentials to a managed provider minimizes custom security code. Centralizing authorization in the router (vs. per-screen checks) ensures new routes are protected by default as the team adds features.
Quality Scenarios:
| Scenario name | Q13 - Blocked Unauthorized Access |
|---|---|
| Quality attributes | Security, Usability |
| App Status and context | No active session exists (logged out, or session cleared). The user attempts to open a protected screen (e.g., Home) directly, such as via app restart or deep link |
| System Reaction | The system detects the missing session and redirects to the Login screen before any protected content is rendered, within 1 second. |
| Scenario name | Q14 - Persistent Session |
|---|---|
| Quality attributes | Security, Usability |
| App Status and context | The user previously logged in successfully. The app is closed completely and reopened later, with the session token still valid. |
| System Reaction | The system restores the session automatically and routes the user to Home, with no re-authentication prompt. |
- Tactic: Graceful degradation and local-first storage
- Quality scenario(s): Q2, Q6, Q8, Q11
- Platform(s): Kotlin
- Responsible: Sara González
Implementation: The activity catalog is bundled in the app as JSON, so recommendations work without internet (with an "Offline-based" label). Missing context (no weather, no interests, no history) contributes a neutral score instead of failing. Usage data is written to Room first, and each day is recalculated from scratch, so repeated or interrupted runs never duplicate or lose data. When usage access is revoked, the Stats view shows a dedicated state with a button to restore it instead of showing zero usage.
Rationale: Recommendations and statistics must remain usable offline, for new users, and when permissions or services are unavailable.
- Tactic: Local-first storage and synchronization retry
- Platform(s): Flutter
- Responsible: Laura Muñoz
Implementation: Focus Session data is first stored locally using SharedPreferences before being synchronized with Firebase Firestore. Each session is marked as unsynced until the upload is successful. If the synchronization fails, the session remains stored locally and can be synchronized later when the backend becomes available.
Rationale: This allows users to complete and preserve Focus Sessions even when the network or backend is temporarily unavailable. Storing the data locally before synchronization reduces the risk of losing session information and supports reliable synchronization with the backend.
- Tactic: Data minimization
- Quality scenario(s): Q9
- Platform(s): Kotlin
- Responsible: Sara González
Implementation: Only approximate location is requested (ACCESS_COARSE_LOCATION), and coordinates are rounded to two decimals (about 1 km) before being sent to the weather API, so the exact position never leaves the device. Per-app usage and activity logs are stored locally. The location request explains that it is only used to check the weather.
Rationale: Weather only needs the user's general area, and usage data is sensitive, so BreakLoop collects and shares the minimum needed for each feature.












