Skip to main content

Summary

The application follows a Clean Architecture approach using MVVM (Model-View-ViewModel) with Jetpack Compose for the UI. It relies heavily on a “Local-First” data strategy where the Room database is the single source of truth for the UI, while background services and workers synchronize data from Android system APIs (Accessibility, UsageStats) into that database. The codebase uses Hilt for dependency injection and Coroutines/Flow for asynchronous operations.

Feature Structure

Most features (screens) are loosely grouped by domain but physically located in ui/ packages. A typical feature consists of:
  1. Screen (Composable): Accepts a ViewModel and Navigation Controller.
  2. ViewModel: Holds StateFlow for UI state, interacts with Repositories.
  3. Repository: Fetches data from DB or System.

Screen Implementation Pattern

Screens typically collect state using collectAsStateWithLifecycle() and pass events back to the ViewModel.

ViewModel Pattern

ViewModels use StateFlow to expose UI state. They often combine multiple data streams (e.g., Usage Data + App Icons + Filter Settings) into a single UI State object. Conventions:
  • Hilt Injection: All ViewModels are annotated with @HiltViewModel.
  • Flow Combination: Heavy use of combine to merge database updates with user filters (e.g., filterSet in ScrollDataRepositoryImpl).
  • Coroutines: viewModelScope is used for launching suspend functions.

Core Architecture: The Tracking Pipeline

The app’s core function—tracking usage and scrolling—uses a specific pipeline to move data from the Android System to the UI.

1. Data Ingestion (The Write Path)

Data enters the system through two primary channels:
  1. Real-time (Accessibility): ScrollTrackService captures scroll events and window changes. It writes raw events directly to raw_app_events via RawAppEventDao.
  2. Historical/Backup (UsageStats): UsageStatsWorker polls Android’s UsageStatsManager to fill in gaps or handle apps where accessibility is disabled.

2. Data Processing (The Aggregation Path)

Raw events are too heavy for direct UI consumption. The app uses a “Process and Summarize” strategy.
  • DailyDataProcessor: A logic class that takes raw events and calculates:
    • Total Screen Time
    • Unlock Counts
    • Scroll Distance (Pixels -> Meters conversion)
  • Trigger: This runs via DailyProcessingWorker (periodic) or ScrollDataRepository.processAndSummarizeDate (on app open).
  • Output: Writes to summary tables (daily_app_usage, daily_device_summary).

3. Real-Time Limit Enforcement

To block apps instantly, the app cannot wait for database writes. It uses a Usage Projection Engine. Formula:
Total Usage = Persisted DB Usage + Pending Buffer + Live Session Duration
  • Persisted: Data already saved in daily_app_usage.
  • Buffer: Sessions that finished in the last few seconds but aren’t in the DB yet.
  • Live: ActiveSessionTracker measures the current foreground session using MonotonicClock.

Dependency Injection (Hilt)

The app uses Hilt for DI. The configuration is centralized in di/.
ModuleFilePurpose
AppSingletonProvidesAppSingletonProvides.ktProvides global singletons: AppDatabase, DAOs, Retrofit, WorkManager, Clock.
AppSingletonBindsAppSingletonProvides.ktBinds interface implementations (e.g., LimitsRepositoryImpl to LimitsRepository).
ViewModelModulesViewModelModules.ktProvides ViewModel-scoped delegates (e.g., LimitViewModelDelegate).
Scopes Used:
  • @Singleton: Used for Repositories, Database, and “Monitor” classes (LimitMonitor, NudgeMonitor) that must maintain state across the app lifecycle.
  • @ApplicationScope: A custom qualifier for a CoroutineScope that survives UI destruction (used for fire-and-forget DB writes).
Navigation is handled by Jetpack Navigation Compose in NavGraph.kt.
  • Routes: Defined as a sealed class ScreenRoutes.
  • Arguments: Passed via route strings (e.g., app_detail/{packageName}).
  • Graph Structure:
    • DASHBOARD_GRAPH: Main tabs (Dashboard, Usage, Unlocks).
    • SETTINGS_GRAPH: Settings, About, FAQ.
    • LIMITS_GRAPH: Limit management.
    • CREATE_LIMIT_FLOW: A nested graph for the multi-step limit creation wizard.

Background Work

The app employs a hybrid approach for background tasks:
ComponentTypeUse CaseImplementation
ScrollTrackServiceAccessibility ServiceReal-time scrolling, app switching detection, overlay drawing.ScrollTrackService.kt
AppTrackerServiceForeground ServiceFallback usage tracking if Accessibility is disabled.AppTrackerService.kt
LimitEnforcementWorkerPeriodic WorkerSafety net to re-check limits every 15 mins.LimitEnforcementWorker.kt
DailyProcessingWorkerPeriodic WorkerAggregates raw data into summaries, runs nightly.DailyProcessingWorker.kt
AppMetadataSyncWorkerOneTime WorkerFetches icons/labels when a new app is installed.AppMetadataSyncWorker.kt

Data Layer Conventions

Repository Pattern

Repositories act as the gatekeepers. They expose Flow<T> for data that changes (UI) and suspend functions for one-off operations.
  • Naming: [Feature]Repository (Interface) -> [Feature]RepositoryImpl (Implementation).
  • Source of Truth: Almost always the local Database. Network calls (like AppCategoryRepository) fetch data and immediately write to the DB; the UI observes the DB, not the network.

Database (Room)

  • Entities: Located in db/.
  • DAOs: Provide Flow return types for reactive updates.
  • Transactions: appDatabase.withTransaction { ... } is used heavily in ScrollDataRepositoryImpl to ensure data consistency when summarizing daily stats.

Common Utilities

Last modified on January 25, 2026