BreakCount Documentation
Technical documentation for the BreakCount Flutter app β architecture, services, themes, achievements, personas, widgets, AI scan, live activity, and nearby.
Quick Links
- breakcount.tech β project website
- Google Play β download the app
- GitHub β source code
Architecture Overview
Project Structure
lib/
βββ app/ # App-level config
β βββ constants.dart # Colors, spacing, StorageKeys
β βββ routes.dart # Named route definitions
β βββ theme.dart # MaterialApp theme builder
β βββ theme_preset.dart # ThemePreset model + AppThemeController
β βββ persona_theme_ext.dart # BuildContext extension for persona tint
βββ screens/ # Full-page UI (tabs, settings, review, etc.)
βββ widgets/ # Reusable UI components
βββ services/ # Business logic, platform channels, storage
βββ data/ # Static data (achievements, personas, school data)
βββ models/ # Data models (Schedule, Subject, Exam, etc.)
βββ utils/ # Helpers (debug_log)
βββ main.dart # Entry point, initialization, BreakCountApp widget
State Management
BreakCount uses a lightweight approach β no external state management package:
ValueNotifier<T>for reactive state (theme, persona tint, streak)StorageService(SharedPreferences wrapper) for persistencesetState()in StatefulWidgets for local UI state- Listeners wired in
main.dartfor cross-cutting concerns
Data Flow
User action β Service method β StorageService.save() β ValueNotifier.value = x
β
Listeners fire
β
Widget rebuilds / side effects
(e.g., WidgetService.update())
Initialization Order (main.dart)
WidgetsFlutterBinding.ensureInitialized()- Firebase init (Core, Crashlytics, Analytics)
StorageService.init()β loads SharedPreferencesAppThemeController.init()β restores saved themeAchievementService.init()β loads unlock stateStreakService.init()+recordOpen()β daily streakPersonaService.init()β restores active persona- Cross-cutting listeners wired (achievementβwidget, personaβwidget, themeβwidget, streakβachievement)
- Notifications, FCM, widget update (fire-and-forget)
runApp(BreakCountApp(...))
Key Patterns
- Fire-and-forget: Non-critical operations (widget updates, analytics, auto-backup) never block startup.
- Graceful degradation: Every service wraps platform calls in try/catch; failures are logged, never propagated.
- Additive architecture: New features (themes, personas, achievements) plug into existing notifier/listener infrastructure without rewrites.
Services Reference
All services live in lib/services/. They are static-method classes (no instantiation needed) unless noted.
Core Services
| Service | Purpose | Key Methods |
|---|---|---|
StorageService | SharedPreferences wrapper | init(), getString(), saveBool(), saveString() |
SchoolDataService | Fetches/caches school year data | fetch(country), getCached(), lastUpdated() |
ScheduleService | CRUD for weekly timetable | getSchedule(), getSubjects(), saveEntry(), clearAll() |
ExamService | CRUD for exams | getExams(), addExam(), deleteExam() |
ReminderService | Exam/break reminders | getUpcomingReminders(), addReminder() |
Feature Services
| Service | Purpose | Key Methods |
|---|---|---|
AchievementService | Unlock tracking, milestone helpers | init(), unlock(id), allUnlocks, onStreakMilestone() |
XpService | XP sum, level, progress | totalXp(), level(), progress(), rankTitle() |
QuestService | Daily quests (3/day) | todayQuests(), incrementProgress(), claimReward() |
StreakService | Daily check-in streak | init(), recordOpen(), currentStreak, longestStreak |
UnlockService | Theme/persona gating | isThemeUnlocked(id), isPersonaUnlocked(id), requirements |
PersonaService | Active persona management | instance.init(), currentNotifier, setPersona(id) |
MoodService | Mood tracking + streaks | currentStreak(kind), recordMood() |
StudyLogService | Manual study session log | logSession(), weeklyBreakdown(), all() |
Platform Services
| Service | Purpose | Key Methods |
|---|---|---|
WidgetService | Pushes data to Android widgets | update() |
LiveActivityService | Lock-screen countdown control | start(), stop(), isAvailable(), isRunning() |
NotificationService | Local notifications | init(), show(), requestPermissions() |
BackupService | Google Drive backup/restore | backup(), restore(), isSignedIn() |
ShakeService | Accelerometer shake detection | startListening(), onShake callback |
MeshService | Nearby Connections P2P | startDiscovery(), sendPayload(), onPeerFound |
AI Services
| Service | Purpose | Key Methods |
|---|---|---|
AiScheduleService | Timetable photo β entries | parseImage(file, apiKey) |
OcrTimetableParser | Offline ML Kit pre-pass | parse(file), parseRecognized(text) |
RecapAiService | Weekly persona recap generation | generateRecap() |
Utility Services
| Service | Purpose |
|---|---|
AnalyticsService | Firebase Analytics event logging |
CalendarService | Export exams to device calendar |
CalculatorService | Break/year calculations |
SubjectImportanceService | Country-aware subject tagging |
Theme System
BreakCount supports 17 theme presets. 6 are always available; 11 unlock via daily streak milestones or achievement triggers.
ThemePreset Model
class ThemePreset {
final String id;
final String name;
final String emoji;
final bool dark;
final Color bgDeep; // Scaffold background
final Color bgDark; // Elevated background
final Color bgSurface; // Card/surface background
final Color primary; // Primary accent
final Color gradA, gradB; // Decorative gradient
final Color surfaceBorder;
final Color? textPrimary; // Override (null = default)
final double cardOpacity;
}
Default Presets (Always Unlocked)
| ID | Name | Emoji | Style |
|---|---|---|---|
coffee | Coffee | β | Warm brown on cream |
midnight | Midnight | π | Dark with indigo accent |
mint | Mint | πΏ | Fresh green |
sakura | Sakura | πΈ | Pink blossom |
ocean | Ocean | π | Blue |
sunset | Sunset | π | Warm orange |
Unlockable Presets
| ID | Unlock Condition |
|---|---|
lavender | 7-day streak |
forest | 14-day streak |
aurora | 30-day streak |
paper | 50-day streak |
cosmic | 75-day streak |
neon | 100-day streak |
amoled | 150-day streak |
vapor | 200-day streak |
solarized | 365-day streak |
zen | Achievement: all_seasonal_breaks |
mono | Achievement: achievement_hunter_50 |
AppThemeController
Global singleton managing the active theme:
AppThemeController.notifierβValueNotifier<ThemePreset>, UI listens to thisAppThemeController.personaTintNotifierβ persona accent color overlayAppThemeController.init(savedId)β restores from storageAppThemeController.setTheme(preset)β switches + persists
Widget Integration
When the theme changes, main.dart's listener calls WidgetService.update(), which writes hex color strings to HomeWidget SharedPreferences. The Android BreakCountWidgetProvider reads these and applies them via RemoteViews.setInt(...).
Adding a New Theme
- Add a
static const ThemePresetintheme_preset.dart - Add it to the
alllist - Add an
UnlockRequiremententry inunlock_service.dart - The theme picker and widget system pick it up automatically
Achievements & XP System
100+ achievements across 8 categories, with an XP/level system and daily quests.
Achievement Model
class Achievement {
final String id;
final String name;
final String description;
final IconData icon;
final AchievementCategory category;
final AchievementRarity rarity;
final int xp; // XP granted on unlock
final bool secret; // Hidden until unlocked
}
Categories
schoolProgress, mondayClub, examsSchedule, breakMilestones, powerUser, streaks, themes, personas
Rarities & XP
| Rarity | XP | Color |
|---|---|---|
| Bronze | 25 | Brown |
| Silver | 75 | Grey |
| Gold | 200 | Gold |
| Platinum | 500 | Purple |
| Secret | 750 | Rainbow |
XP / Level System
Thresholds: L1=0, L2=200, L3=600, L4=1500, L5=3500, L6=7000, L7=12000, L8=20000, L9=35000, L10=60000
| Level | Rank Title |
|---|---|
| 1 | Newcomer |
| 2 | Rookie |
| 3 | Survivor |
| 4 | Veteran |
| 5 | Master |
| 6 | Legend |
| 7 | Mythic |
| 8 | Ascendant |
| 9 | Transcendent |
| 10+ | Eternal |
Daily Quests
3 quests per day, deterministically seeded by date + install ID. ~20 quest templates (e.g., open_schedule_tab, tap_countdown_5x, change_persona). Completion grants XP; some high-tier quests unlock themes/personas. Reset at local midnight.
Milestone Helpers
AchievementService exposes helpers that other services call:
onStreakMilestone(int days)β called by StreakServiceonThemeUnlocked(String id)β called by UnlockServiceonPersonaUnlocked(String id)β called by PersonaServiceonStudySessionLogged()β called by StudyLogServiceonAchievementCountChanged()β self-referential (hunter ladder)
Adding Achievements
- Add entry to
lib/data/achievements_data.dartwith uniqueid, category, rarity, XP - Add unlock trigger in the appropriate service helper
- Run
test/achievements_data_test.dartto verify no duplicate IDs
Personas
30 personas, each with a unique emoji, tint color, and full copy palette. The active persona affects app-wide text, colors, confetti, widgets, and weekly recaps.
Persona Model
class Persona {
final String id;
final String name;
final String emoji;
final Color tint; // Accent color override
final bool isDefault; // Always unlocked
}
Copy System
Copy is keyed by (personaId, slot). Slots include:
countdown_farβ 60+ dayscountdown_midβ 15β60 dayscountdown_closeβ 2β15 dayscountdown_imminentβ 0β2 daysbreak_revealβ break just startedrecap_introβ weekly recap openergreetingβ app open
Base Personas (Always Unlocked)
| ID | Name | Emoji | Tint |
|---|---|---|---|
hype | Hype | π₯ | Orange-red |
chill | Chill | π | Blue |
dramatic | Dramatic | π | Purple |
sarcastic | Sarcastic | π | Teal |
Unlockable Personas
26 additional personas unlock via streak milestones and achievement triggers, registered in UnlockService. Includes: Ghost π», Sage π§, Nerd π€, Tired π₯±, Ice π§, Philosopher π§, Volcano π, Sloth π¦₯, Moon π, Phoenix π¦
, Jester π, Hacker π», Pirate π΄ββ οΈ, Robot π€, and more.
PersonaService
PersonaService.instanceβ singletoncurrentNotifierβValueNotifier<Persona>, drives UI rebuildssetPersona(id)β switches active persona, updates tint notifier, persistsinit()β restores from storage
Persona Tint Integration
AppThemeController.personaTintNotifier is updated whenever the persona changes. The MaterialApp rebuilds with the new tint via persona_theme_ext.dart's BuildContext extension.
Adding a Persona
- Add entry to
lib/data/personas_data.dart - Add copy entries for all slots in
lib/data/persona_copy.dart - Add unlock condition in
lib/services/unlock_service.dart - Run
test/persona_copy_test.dartto verify all slots are covered
Home Screen Widgets
Four widget sizes (2Γ1, 2Γ2, 4Γ1, 4Γ2) powered by native Android AppWidgetProvider + the home_widget Flutter package.
Architecture
Flutter (WidgetService.update())
β writes key-value pairs
HomeWidget SharedPreferences
β read by
Android BreakCountWidgetProvider (Kotlin)
β applies to
RemoteViews β AppWidgetManager.updateAppWidget()
Data Payload
| Key | Type | Description |
|---|---|---|
days_until_break | int | Days to next break (-1 if none) |
next_break_name | String | Break name |
year_progress | int | 0β100 |
is_on_break | bool | Currently on break |
vibe_emoji | String | Persona emoji |
vibe_copy | String | Mood-aware one-liner |
current_class | String? | Active class name |
theme_bg_hex | String | Theme background (#RRGGBB) |
theme_primary_hex | String | Primary accent |
theme_dark | bool | Dark mode flag |
persona_tint_hex | String | Persona accent color |
Update Triggers
WidgetService.update() is called on app startup, when the theme changes, when the persona changes, when an achievement unlocks, and on background widget interaction.
Layout Files
breakcount_widget_2x1.xmlβ horizontal pillbreakcount_widget_2x2.xmlβ square card with progress ringbreakcount_widget_4x1.xmlβ wide barbreakcount_widget_4x2.xmlβ wide card
AI Timetable Scan & Offline OCR
Offline-first timetable import: ML Kit OCR runs locally first, cloud providers are only called as a fallback.
Flow
User takes photo
β
AiScheduleService.parseImage(file, apiKey)
β
ββ OcrTimetableParser.parse(file) βββ offline pre-pass
β ββ ML Kit TextRecognizer
β ββ Grid detection (rows by Y-coordinate)
β ββ Day column detection (header tokens)
β ββ Subject matching (canonical suggestions)
β ββ Confidence scoring
β
ββ If confidence β₯ 0.6 AND entries β₯ 20 β return (isOfflineOcr: true)
β
ββ Else fall through to cloud provider:
ββ Groq Llama 4 (if key starts with gsk_)
ββ Cloudflare Worker proxy (5 free/day, no key needed)
β
AiReviewScreen (edit/delete entries per day)
β
ScheduleService.save()
Offline OCR Algorithm
- Run
TextRecognizer(script: latin)on the image - Flatten recognized blocks into lines with bounding box coordinates
- Group lines into rows by Y-coordinate (tolerance = 0.7Γ avg line height)
- Detect day columns from header row (Mon/Tue/Wed/Thu/Fri in multiple languages)
- Walk data rows, match text against country's canonical subject list
- Assign each match to nearest day column
- Deduplicate by (subject, day, startTime)
Confidence Scoring
confidence = entryScore Γ 0.6 + daysScore Γ 0.25 + subjectHitRate Γ 0.15
entryScore = entries found / 20 (clamped 0β1)
daysScore = distinct days covered / 5
subjectHitRate = matched entries / total recognized lines
Threshold: β₯ 0.6 confidence AND β₯ 20 entries β skip cloud call
Cloud Providers
- Groq Llama 4 β endpoint:
api.groq.com/openai/v1/chat/completions, model:meta-llama/llama-4-scout-17b-16e-instruct. Free key from console.groq.com. - Worker Proxy β endpoint:
breakcount-ai.breakcount.workers.dev. Rate limit: 5 scans/day per device ID. No API key required.
Review Screen
AiReviewScreen shows entries day-by-day (MonβFri stepper). Features: edit subject name with autocomplete, pick start/end time, cycle color, swipe to delete, add new entries, and a green "Parsed offline" banner when isOfflineOcr is true.
Lock-Screen Live Activity
Android 14+ foreground service that shows a persistent break countdown notification on the lock screen. Uses Chronometer for battery-efficient live ticking.
Architecture
Flutter (LiveActivityService)
β MethodChannel 'com.breakcount/live_activity'
MainActivity.kt (channel handler)
β startForegroundService / stopService
BreakCountdownService.kt
β reads widget data from HomeWidget SharedPreferences
β builds Notification with Chronometer
Lock screen / notification shade
Method Channel API
Channel: com.breakcount/live_activity
| Method | Returns | Description |
|---|---|---|
start | bool | Starts service (false if < API 34) |
stop | bool | Stops service |
isRunning | bool | Service active state |
isAvailable | bool | Device supports it (API 34+) |
BreakCountdownService (Kotlin)
- Foreground service type:
specialUse - Notification channel:
breakcount_countdown(IMPORTANCE_LOW) - Ongoing, silent, public visibility with Chronometer counting down to break start
- Handler posts every 15 minutes to refresh notification data
Permissions
FOREGROUND_SERVICEβ base permissionFOREGROUND_SERVICE_SPECIAL_USEβ required for specialUse type on API 34+POST_NOTIFICATIONSβ required to show the notification
Nearby / Mesh System
BreakCount uses Android Nearby Connections (Bluetooth) for peer-to-peer features: schedule sharing, Vibe Beacon, and achievement compare.
Discovery Flow
startDiscovery() β onPeerFound callback
β
User taps peer card
β
requestConnection() β onConnectionEstablished
β
sendPayload(data) β receivePayload(data)
β
disconnect()
Handshake Payload
{
"type": "handshake",
"name": "Anonymous Student",
"persona_id": "hype",
"persona_emoji": "π₯",
"subject_count": 12,
"entry_count": 35,
"unlocked_achievement_ids": ["streak_7", "first_exam", ...]
}
Features
Schedule Copy
Peer card shows subject/entry count. "Copy" button pulls their schedule entries to your device. Merge/replace dialog if you already have entries.
Vibe Beacon
Long-press Vibe card β radar overlay. Groups discovered peers by persona. Real-time discovery while overlay is open.
Achievement Compare
Computes three sets from the handshake payload: Both have (intersection), Only mine, and Only theirs. Displayed as a bottom sheet from the nearby users screen.
Shake to Share
ShakeService detects 20 m/sΒ² threshold. Opens share overlay when both devices shake simultaneously. Uses the same Nearby Connections infrastructure.
Privacy
- No real names exchanged β anonymous display names only
- Achievement IDs are opaque strings (no personal data)
- Opt-in via Settings β Social toggle (
vibeBeaconEnabled) - Bluetooth/Location permissions required