An offline-first Android personal-finance app with an AI assistant, OCR receipt scanning, monthly budgets with rollover, recurring subscriptions, and a home-screen Quick-Add widget. Built with Kotlin, Room, WorkManager, and a pluggable LLM provider layer (on-device Gemini Nano or any OpenAI-compatible cloud).
- Package:
mobile.com.expense_tracking_app - Min SDK: 26 (Android 8.0) · Target/Compile SDK: 36
- Language: Kotlin 2.1 · Build: AGP 9.1, Gradle wrapper
- Default currency: VND (Vietnamese đồng)
- Today / monthly / yearly spending totals with day-over-day and month-over-month deltas
- Spending-trend chart (MPAndroidChart) and category breakdown
- "AI Action Recommendation" card surfacing on-device or cloud-generated insights
- Recent transactions list with quick edit/delete
- Bottom-sheet entry with amount quick-pick, category picker, notes, and date
- One-tap Quick-Add templates (chips above the form mirror the home-screen widget)
- Receipt scanning — camera or gallery → ML Kit OCR → LLM-powered line-item extraction → review/edit each line before saving
- Optional "Recurring" toggle promotes a one-off entry into a subscription
- Per-category monthly limits with progress bars and alert thresholds
- Calendar navigation — view past months read-only; future navigation is bounded (no >12 months ahead)
- Rollover unused budget into the next month
- Temporary budget override for the current month without changing the default
- Monthly archival handled in the background by
BudgetArchiveWorker
- Weekly / monthly / yearly cycles with optional expiry date
- Auto-record into transactions via
ExpenseAutoInsertWorker - "Due soon" filter, pause/resume, validation against past dates and expiry-before-billing
- Optional reminder before billing date
- Pluggable provider registry — choose any of:
- On-device: Gemini Nano via ML Kit GenAI (Android 14+, supported Pixel/Samsung devices)
- Cloud: Google Gemini, OpenAI, Anthropic Claude, DeepSeek, Groq, OpenRouter, or any OpenAI-compatible endpoint
- Streams responses via OkHttp SSE
- Tool-calling: the assistant can propose actions (log expense, add category, edit budget, create subscription) which you confirm in-app before they touch the database
- API keys stored in
EncryptedSharedPreferences - "Prefer on-device when available" toggle to keep data local
- Quick-Add widget with swipeable template chips and a today-total readout
- Tap a chip →
QuickAddInsertWorkerlogs the transaction in the background - "+ Add" tile deep-links into
MainActivityand opens the Add Expense sheet
- Username + avatar (12 built-in avatars)
- Manage spending categories (icon, color, monthly budget — up to a hard cap)
- Manage Quick-Add templates (label, amount, optional note)
- AI configuration — separate slots for chat provider and receipt-parsing provider, per-provider API key vault, connection test, model override, custom base URL for OpenAI-compatible endpoints
Feature-module layout with shared core/ for data, AI, OCR, workers, and helpers. Each feature owns its Fragment + ViewModel + adapters; ExpenseTrackingApplication is the manual dependency-injection root (no Hilt/Koin).
mobile.com.expense_tracking_app/
├── ExpenseTrackingApplication.kt # Repository graph + WorkManager bootstrap
├── MainActivity.kt # Bottom-nav host + FAB routing
├── core/
│ ├── data/
│ │ ├── AppDatabase.kt # Room v4, destructive migration, category seed
│ │ ├── entities/ # Transaction, Category, BudgetLog, Subscription,
│ │ │ # TransactionTemplate, Transfer, PaybackTransaction
│ │ ├── dao/ # One DAO per entity
│ │ ├── repositories/ # Thin wrappers exposing Flow<...> APIs
│ │ ├── relations/ # Room @Relation projections
│ │ └── preferences/ # UserPreferences (DataStore-like wrapper)
│ ├── ai/
│ │ ├── AiRepository.kt # Orchestrates provider, prompts, tool execution
│ │ ├── AiPreferences.kt # Encrypted per-provider key vault
│ │ ├── LlmProviderRegistry.kt # Registers + selects providers
│ │ ├── providers/ # OpenAI, Anthropic, Gemini, ML Kit, SSE client
│ │ ├── PromptTemplates.kt
│ │ ├── ProposedAction.kt # Confirm-before-execute action payloads
│ │ ├── ToolExecutor.kt # Applies confirmed actions to repositories
│ │ ├── AiContextBuilder.kt # Snapshots spending context for prompts
│ │ └── RecommendationEngine.kt # Dashboard "AI Action" card
│ ├── ocr/
│ │ ├── MlKitOcrEngine.kt # ML Kit text recognition
│ │ ├── LlmReceiptParser.kt # OCR text → structured line items via LLM
│ │ └── ReceiptPrompt.kt
│ ├── workers/
│ │ ├── BudgetArchiveWorker.kt # Rolls over budgets at month boundary
│ │ └── ExpenseAutoInsertWorker.kt # Posts due recurring expenses
│ ├── alarms/ # Subscription reminders
│ ├── notifications/
│ ├── helpers/ # Formatters, date utils, currency
│ ├── models/ # UI-layer models
│ └── ui/ # Reusable custom views (AmountQuickPickView, …)
├── feature_dashboard/
├── feature_expense/ # Add expense + transaction list + receipt review
├── feature_assistant/ # Chat UI
├── feature_budget/
├── feature_recurring/
├── feature_settings/ # Profile, categories, templates, AI config
├── feature_widget/ # Quick-Add app widget
└── feature_lending/ # Hidden in v1.0 — see RELEASE.md §7
| Layer | Choice |
|---|---|
| Language | Kotlin 2.1 + Coroutines + Flow |
| UI | View Binding, Material Components, ConstraintLayout, RecyclerView |
| Navigation | AndroidX Navigation 2.8 (single-Activity, bottom nav) |
| State | ViewModel + LiveData |
| Persistence | Room 2.8 (KSP) + EncryptedSharedPreferences |
| Background work | WorkManager 2.10 |
| Charts | MPAndroidChart 3.1 |
| OCR | ML Kit Text Recognition |
| On-device LLM | ML Kit GenAI (Prompt + Summarization, beta) |
| Cloud LLM | OkHttp 4.12 + OkHttp-SSE, Kotlinx Serialization |
| Testing | JUnit, Turbine, Room-testing, Work-testing, Espresso, ArchCoreTest |
Dependencies are version-catalogued in gradle/libs.versions.toml.
- Android Studio (Hedgehog or newer recommended)
- JDK 17 (AGP 9 requires it)
- Android SDK 36
- A device or emulator running API 26+
git clone <repo-url>
cd ExpenseTrackingAppOpen the project in Android Studio and let Gradle sync.
./gradlew :app:installDebugOr hit Run ▶ in Android Studio with a device connected.
The app runs without any AI key — the chat tab and receipt scanner simply show a "set up AI" prompt. To enable them:
- Launch the app → Settings → AI configuration.
- Pick a provider, paste an API key, optionally choose a model.
- (Cloud-only) Tap Test connection to verify.
To pre-seed a DeepSeek key for development, add to local.properties:
deepseekApiKey=sk-...This is baked into BuildConfig.DEEPSEEK_API_KEY and migrated into the encrypted vault on first launch. Leave this blank for production builds — users supply their own keys.
Declared in AndroidManifest.xml:
| Permission | Why |
|---|---|
INTERNET |
Cloud LLM calls, OCR receipt parsing |
ACCESS_NETWORK_STATE |
Pre-flight connectivity check for AI features |
CAMERA |
Receipt photo capture (declared optional) |
POST_NOTIFICATIONS |
Subscription due-date reminders |
SCHEDULE_EXACT_ALARM |
Exact-time reminders for recurring expenses |
The camera hardware feature is declared required="false" so the app installs on devices without one.
Room v4, single expense_tracker_database file. The schema is not exported and uses fallbackToDestructiveMigration — pre-release a wipe is acceptable; for v1.1+ you'll want to switch to explicit migrations.
Entities:
Transaction— every logged expense (amount, category FK, note, date, receipt ref)Category— name, icon, color, default monthly budget, temp-budget overrideBudgetLog— historical per-month snapshots written byBudgetArchiveWorkerSubscription— recurring expense definitionsTransactionTemplate— Quick-Add chips shown in widget + add-expense sheetTransfer+PaybackTransaction— lending/borrowing (feature hidden in v1.0)
A SeedCallback inserts six default categories with zero budgets on first launch.
./gradlew test # JVM unit tests
./gradlew connectedAndroidTest # Instrumented tests (device/emulator required)Instrumented tests live under app/src/androidTest and cover Room DAOs, repositories, WorkManager workers, and ViewModel flows (with Turbine).
Production builds (signed App Bundle for Google Play) are documented in detail in RELEASE.md. Quick reference:
./gradlew :app:bundleRelease # Play Store AAB
./gradlew :app:assembleRelease # Sideloadable APKR8 + resource shrinking are enabled for release. Keystore credentials are read from local.properties (gitignored). Without them, the release build still completes but produces an unsigned APK.
- v1.0 (current) — Dashboard, Add Expense, Budget, Recurring, Assistant, Widget, Settings
- v1.1+ — Re-enable the Lending/Transfer tab (code is preserved; see RELEASE.md §7)
- Explicit Room migrations (drop
fallbackToDestructiveMigration) - Multi-currency support (currently VND-only)
- Schema export for migration testing
Not yet specified.