Skip to main content

Summary

The application uses Hilt (built on top of Dagger) for dependency injection. This setup centralizes the creation of complex objects like the database, network clients, and repositories, ensuring they are available throughout the app without manual instantiation. Dependencies are wired at compile time, and the graph is managed automatically based on the lifecycle of Android components (Application, Activity, ViewModel).

Dependency Graph

This diagram illustrates how data flows from low-level infrastructure up to the UI layer via Hilt injection.

Modules

The dependency graph is defined primarily in AppSingletonProvides.kt, which is split into providers (creating objects) and bindings (mapping interfaces).

AppSingletonProvides

Type: @InstallIn(SingletonComponent::class) Purpose: Creates and configures third-party libraries, system services, and the database.
DependencyScopeDescription
AppDatabaseSingletonThe Room database instance, configured with extensive migration logic (v28 through v44).
DAOsSingleton14+ Data Access Objects (e.g., UserDao, ScrollSessionDao) extracted from the database.
NetworkSingletonOkHttpClient (with logging), Retrofit (Gson converter), and CategoryApiService.
CoroutinesSingletonProvides Dispatchers (IO, Main) and a global applicationScope.
SystemSingletonWorkManager, ConnectivityManager, Clock, and DateUtil.

AppSingletonBinds

Type: @InstallIn(SingletonComponent::class) Purpose: Binds implementation classes to their repository interfaces. This allows the app to depend on abstractions (Interfaces) rather than concrete implementations.
InterfaceImplementation
LimitsRepositoryLimitsRepositoryImpl
AppMetadataRepositoryAppMetadataRepositoryImpl
SettingsRepositorySettingsRepositoryImpl
ScrollDataRepositoryScrollDataRepositoryImpl
AppCategoryRepositoryAppCategoryRepositoryImpl
…and others

ViewModelModules

Type: @InstallIn(ViewModelComponent::class) Purpose: Provides dependencies that live only as long as a ViewModel (and its associated screen).
DependencyDescription
LimitViewModelDelegateLogic delegate for handling limit enforcement within ViewModels.

Scopes and Qualifiers

The app uses standard Hilt scopes alongside custom qualifiers to disambiguate dependencies (specifically for threading).

Lifecycle Scopes

ScopeAnnotationLifetimeUsed For
Application@SingletonEntire App ProcessDatabase, Network, Repositories, Global Config.
ViewModel@ViewModelScopedScreen LifecycleUI-specific logic delegates.
Activity@ActivityScopedActivity LifecycleDependencies bound to MainActivity.
Unscoped(None)On DemandLightweight objects created fresh every time they are injected.

Custom Qualifiers

Defined in di/Scopes.kt, these annotations ensure the correct thread or configuration is injected.
QualifierUsage
@IoDispatcherInjects Dispatchers.IO for background operations.
@MainDispatcherInjects Dispatchers.Main for UI updates.
@ApplicationScopeInjects a CoroutineScope tied to the application lifecycle (for processes that must survive screen rotation/closure).
@Named("app_category_prefs")Injects the specific SharedPreferences file for app categories.

Key Bindings

ComponentCreation Logic
AppDatabaseCreated via Room.databaseBuilder. Includes a fallback strategy (fallbackToDestructiveMigrationOnDowngrade) to handle development version mismatches safely.
DAOsProvided directly from the database instance (e.g., db.scrollSessionDao()).

How to Add Dependencies

Follow this guide when adding new features to the app.
RequirementActionFile
New Repository1. Create Interface & Impl
2. Add @Binds method
di/AppSingletonProvides.kt (inside AppSingletonBinds)
New DAO1. Add to AppDatabase
2. Add @Provides method
di/AppSingletonProvides.kt
New ViewModelAnnotate class with @HiltViewModel and constructor with @InjectThe ViewModel file itself
New WorkerAnnotate class with @HiltWorker and constructor with @AssistedInjectThe Worker file itself

Rules & Best Practices

  1. Prefer Constructor Injection: Always try to inject dependencies into the constructor (@Inject constructor(...)) rather than using field injection.
  2. Use Qualifiers for Primitives: If injecting a String, Long, or generic interface like CoroutineDispatcher, always use a Qualifier (e.g., @IoDispatcher) to avoid ambiguity.
  3. Avoid Singleton Abuse: Only mark a dependency as @Singleton if it holds state or is expensive to create (like a Database or Network Client). Stateless helpers usually don’t need a scope.
  4. Database Migrations: When modifying the database schema, the migration logic must be added to the AppDatabase builder in AppSingletonProvides.
Last modified on January 25, 2026