|
Some checks are pending
CI / build (push) Waiting to run
The 3.x arc is live on Play and the next work is owner-directed. §8 said so in prose only, which reads to the loop as a missing milestone rather than a finished roadmap. milestone-cycle v1.12.0 writes one canonical marker and re-parses it before committing, so this file is now round-trip clean against the loop's own parser. |
||
|---|---|---|
| .forgejo/workflows | ||
| .github/workflows | ||
| app | ||
| design | ||
| design-system | ||
| docs | ||
| gradle/wrapper | ||
| store-assets | ||
| tools | ||
| .cgcignore | ||
| .gitignore | ||
| AGENTS.md | ||
| build.gradle.kts | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| DESIGN-3.1.md | ||
| DESIGN-3.md | ||
| DESIGN.md | ||
| GEMINI.md | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.md | ||
| ROADMAP.md | ||
| settings.gradle.kts | ||
| TODOS.md | ||
Debt Tracker
A native Android app for tracking shared expenses and debts between people. Built with Kotlin and Jetpack Compose.
Fully offline, no accounts, no ads, no tracking.
v3.0.1 live on Google Play (2026-06-21, 100% production) — three dogfood-driven fixes on top of 3.0: pay back group debts in any amount (partial / multiple payments), the Add-debt control asks who owes who, and the Groups screen shows a member count. See
CHANGELOG.md.v3.0.0 (2026-06-12) — full Splitwise feature parity, kept entirely local: groups with five split modes and multiple payers, fewest-payments settlement, categories, an activity feed, search, monthly totals, receipts, and comments — plus a four-tab shell and an entry-flow redesign from real user feedback. 1324 tests (unit, snapshot, accessibility). See
docs/ROADMAP_3.0.mdandDESIGN-3.md.v2.0.0 — the visual foundation: 20-component design system, 12 curated themes, WCAG AA contrast, reduced motion support. See
DESIGN.md.
Why this exists
Splitwise — long the default for splitting bills with friends — added a 3-5 expense/day cap to its free tier in late 2024, with a $40/year Pro tier to remove it. Debt Tracker is for people who want to do the same job without an account, without ads, without a daily limit, and without a paywalled feature list.
| Splitwise (free) | Debt Tracker | |
|---|---|---|
| Daily expense limit | 3-5/day | Unlimited |
| Account required | Yes | No |
| Ads | Yes | Never |
| Pro tier | $40/year | $0 forever |
| Works offline | No | Yes |
| Open source | No | GPL-3.0 |
| Cross-user cloud sync | Yes | No (by design) |
Honest comparison including where Splitwise wins: vs Splitwise.
Features
- Track People — Add anyone you share expenses with
- Log Transactions — Record who owes whom, with descriptions and dates
- Automatic Balances — See at a glance who owes you and who you owe
- Recurring Charges — Set up automatic monthly expenses (rent, subscriptions, etc.) with immediate first fire, full editing (amount, description, type, frequency, next-due date), on-demand "charge now" trigger, and 12-month backfill limit
- Transaction History — View, edit, or delete past transactions
- Backup & Restore — Export/import data as JSON files
- Share Backups — Send backup files via email, cloud storage, or messaging apps
- Automatic Backups — Schedule daily, weekly, or monthly backups. Stored outside the app via SAF so they survive uninstall. Snapper-style retention (3 daily, 2 weekly, 1 monthly).
- Backup Browser — Browse and restore from automatic backups with human-readable dates, retention badges, and entity counts
- Colour Themes — 12 themes across Everyday, Mood, and Terminal groups (v2.0). Theme choice persists across app restarts.
Privacy
All data stays on your device. No internet connection needed, no accounts required, no analytics or tracking. Your financial information is never uploaded anywhere.
Tech Stack
- Language: Kotlin
- UI: Jetpack Compose with Material3
- Architecture: MVVM with ViewModel + Repository
- Database: Room (SQLite) with atomic transactions
- Async: Kotlin Coroutines & Flow
- Serialization: Gson
- Screenshot Testing: Paparazzi (
:design-systemmodule) - Min SDK: 26 (Android 8.0)
- Target SDK: 35 (Android 15)
Building
Requirements
- JDK 17 or 21
- Android SDK 35
Debug Build
./gradlew assembleDebug
APK output: app/build/outputs/apk/debug/app-debug.apk
Release Build
Requires keystore.properties in the project root with signing config.
./gradlew bundleRelease
AAB output: app/build/outputs/bundle/release/app-release.aab
Testing
# Unit tests
./gradlew test
# Record Paparazzi screenshot goldens (after visual changes)
./gradlew :design-system:recordPaparazziDebug
# Verify screenshots match goldens (CI / pre-merge)
./gradlew :design-system:verifyPaparazziDebug
Golden images are committed at design-system/src/test/snapshots/images/. The verify task fails if any component renders differently from its golden — run recordPaparazziDebug to update after intentional visual changes.
Project Structure
design-system/src/main/java/com/debttracker/ds/
├── DtTheme.kt # Entry point composable
├── DtThemeId.kt # 12-theme enum + mode/group flags
├── ThemeResolver.kt # DtThemeId → ResolvedTheme (colours, typo, shape)
├── tokens/
│ ├── DtColors.kt # Semantic colour tokens
│ ├── DtMotion.kt # Animation timing tokens
│ ├── DtSpacing.kt # 4dp grid spacing scale
│ ├── DtShape.kt # Corner radius tokens (xs/sm/md/lg/full/pill)
│ └── DtTypography.kt # Type scale + font families
├── themes/
│ └── (12 files) # One palette per theme (Paper★, Midnight, Sage, …)
├── components/
│ ├── DtButton.kt # 5 variants, 3 sizes, loading state
│ ├── DtIconButton.kt # Icon-only button with danger toggle
│ ├── DtCard.kt # Elevated card with hero variant
│ ├── DtListRow.kt # M3 ListItem wrapper
│ ├── DtTopBar.kt # Top app bar with scroll behaviour
│ ├── DtFab.kt # FAB + extended FAB
│ ├── DtAvatar.kt # Deterministic colour avatar
│ ├── DtPill.kt # 4-intent status pill
│ ├── DtBadge.kt # Compact inline badge
│ ├── DtTextField.kt # Outlined text input with error slot
│ ├── DtAmountField.kt # Hero numeric input (no system IME)
│ ├── DtNumericKeypad.kt # 4×3 keypad with TalkBack labels
│ ├── DtDateField.kt # Tap-to-open M3 DatePicker + presets
│ ├── DtChip.kt # Selectable filter chip (token-aware)
│ ├── DtConfetti.kt # Canvas-based celebration particles
│ ├── DtDialog.kt # M3 dialog wrapper + DtAction
│ ├── DtBottomSheet.kt # M3 ModalBottomSheet wrapper
│ ├── DtEmojiChip.kt # 28dp emoji circle (transaction type icons)
│ ├── DtEmptyState.kt # Centred no-data state with CTA
│ ├── DtSkeleton.kt # Rounded-rect shimmer placeholder
│ └── DtSnackbar.kt # Undo-capable snackbar with accent strip
├── util/
│ ├── DtFormatAmount.kt # Single-source currency formatter
│ ├── DtFormatAmountWords.kt # TalkBack-friendly amount-to-words
│ └── DtReducedMotion.kt # System reduced motion detection
└── preview/
├── DtThemePreview.kt # Token showcase preview
└── ComponentPreviews.kt # All components × 12 themes
app/src/main/java/com/debttracker/app/
├── MainActivity.kt
├── DebtTrackerApplication.kt # Application: owns the singleton DB + repository
├── data/
│ ├── backup/
│ │ ├── AutoBackupManager.kt # Backup scheduling & preferences
│ │ ├── AutoBackupWorker.kt # WorkManager background worker
│ │ └── BackupRetention.kt # Snapper-style retention algorithm
│ ├── database/
│ │ ├── DebtTrackerDao.kt # Room DAOs
│ │ └── DebtTrackerDatabase.kt # Room database
│ ├── model/
│ │ ├── Person.kt # Person entity
│ │ ├── Transaction.kt # Transaction entity
│ │ ├── RecurringCharge.kt # Recurring charge entity
│ │ ├── PersonLastActivity.kt # DAO query result for recency grouping
│ │ └── BackupData.kt # Backup/restore data class
│ └── repository/
│ └── DebtRepository.kt # Repository layer
└── ui/
├── AppStartupViewModel.kt # Launch-time recurring processing + backup
├── HomeViewModel.kt # Home: persons, recency, search, sticky pre-fill
├── PersonViewModel.kt # Person detail (keyed per personId)
├── RecurringViewModel.kt # Recurring charges (keyed per personId)
├── SettingsViewModel.kt # Backup export / restore / clear-all
├── BackupBrowserViewModel.kt # SAF backup-folder browse + restore
├── SnackbarEvent.kt # Undo-carrying snackbar events
├── navigation/
│ └── AppNavState.kt # Typed back-stack holder (replaced enum Screen)
├── components/
│ └── PersonPicker.kt # Domain-coupled avatar row + "Add new"
└── screens/
├── AddExpenseSheet.kt # v2.0: bottom sheet with amount, person, type, date
├── BackupBrowserScreen.kt # Browse & restore backups
├── Dialogs.kt
├── HomeScreen.kt # v2.0: hero card, recency groups, search, FAB
├── HomeScreenHelpers.kt # Pure functions for balance/recency/search logic
├── TransactionHistoryScreen.kt # v2.0: person header, sparkline, time-grouped txns
├── PersonScreenHelpers.kt # Pure functions for time grouping, sparkline, labels
├── RecurringChargesScreen.kt
├── SettingsScreen.kt
└── SettleUpDialog.kt # v2.0: settle-up confirmation with formatted balance
Publishing to Google Play
The app uses Gradle Play Publisher for automated releases.
# Build and publish to production
./gradlew publishReleaseBundle
# Upload store listing only (description, release notes)
./gradlew publishListing
- Service account:
~/.config/play-store/service-account.json - Track: production
- Release notes:
app/src/main/play/release-notes/en-US/default.txt - Store listing:
app/src/main/play/listings/en-US/
Known Issues
Play Store edge-to-edge warnings: Google's static analysis flags setStatusBarColor, setNavigationBarColor, and LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES. These originate from AndroidX library bytecode (EdgeToEdgeApi26, EdgeToEdgeApi29 in activity-compose, SystemJobInfoConverter in work-runtime), not app code. The deprecated APIs provide backward compatibility for pre-Android 15 devices and cannot be removed by app developers. The app displays correctly edge-to-edge on Android 15. This is a known ecosystem issue.