7.8 KiB
AGENTS.md
Project Overview
Personal finance Flutter app for Android. Tracks accounts, transactions, categories, budgets with multi-currency support, biometric auth, and haptic feedback.
Stack
- Flutter — UI framework
- Riverpod — state management (per-feature
provider.dartfiles) - Drift — local database (code generation via
.g.dart) - Feature-first architecture
Project Structure
lib/
├── app/ # App root, router, theme
├── core/ # Constants, l10n, core services, utils
├── data/ # Database schema, repositories
├── features/ # Feature modules (provider + screen + widgets)
├── shared/ # Cross-feature models, providers, services, widgets
│ ├── feature_flags/
│ │ ├── feature_flags.dart # abstract class FeatureFlags
│ │ ├── free_feature_flags.dart # FreeFeatureFlags implements FeatureFlags
│ │ ├── vip_feature_flags.dart # VipFeatureFlags implements FeatureFlags
│ │ └── feature_flags_provider.dart # featureFlagsProvider
│ └── paywall/
│ ├── paywall_guard.dart # PaywallGuard widget
│ ├── paywall_banner.dart # inline upsell banner
│ └── paywall_screen.dart # full-screen paywall
└── main.dart
Layer Responsibilities
core/ — app-wide infrastructure. No business logic.
constants.dart— global constantsl10n/— localization strings and locale providerservices/— biometric, haptic, card color servicesutils/result.dart—Result<T>type for error handling
data/ — persistence only. No UI, no Riverpod providers.
database/— Drift tables and generated code. Do not edit.g.dartfiles manually.repositories/—AccountRepository,TransactionRepository. All DB access goes through repositories.
features/<name>/ — self-contained feature modules.
provider.dart— Riverpod providers scoped to this featurescreen.dart— top-level screen widget, minimal logicwidgets/— feature-specific widgets
shared/ — reusable across features.
models/—Account,Transactiondata classesproviders/— providers used by multiple featuresservices/—ExchangeRateService,StorageServiceutils/—CurrencyUtilswidgets/—BynSign,ErrorSnackbarfeature_flags/—FeatureFlagsabstraction and plan-specific implementationspaywall/—PaywallGuard,PaywallBanner,PaywallScreen
Architecture Rules
- Widgets never access repositories directly — always through providers
- Providers in
features/<name>/provider.dartare local to that feature - Providers in
shared/providers/are app-wide - Business logic lives in providers or repositories, not in widgets or screens
Result<T>fromcore/utils/result.dartis used for fallible operations in repositories- Database queries are in repositories only — no raw Drift queries in providers or widgets
- After any changes to Drift tables or DAOs, run
dart run build_runner build --delete-conflicting-outputs
Feature Gating Rules
- Never check
user.isViporplan == UserPlan.vipdirectly in widgets or screens - All access control goes through
featureFlagsProvider— read the relevant flag, wrap withPaywallGuard - Quantity limits (e.g. max accounts) are enforced inside repositories, not in widgets — throw
FeatureLimitExceptionon violation - Routes that are entirely VIP-only use GoRouter
redirectreadingfeatureFlagsProvider - Adding a new gated feature means: add a getter to
FeatureFlags, implement inFreeFeatureFlagsandVipFeatureFlags, then use in UI/repo
Code Style
No comments anywhere in the codebase. No //, no /* */, no /// doc comments. Code must be self-explanatory through naming.
- Use descriptive names — no abbreviations unless universally known (
id,url,db) - Prefer named constructors and named parameters
- Extract widgets aggressively — if a build method exceeds ~40 lines, split it
- Widget files: one primary widget per file, name matches filename
- Use
finaleverywhere possible - No
dynamictypes - Avoid
lateunless genuinely necessary
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Files | snake_case.dart |
balance_card.dart |
| Classes | PascalCase |
BalanceCard |
| Providers | camelCase + Provider suffix |
transactionListProvider |
| Riverpod notifiers | PascalCase + Notifier suffix |
AddTransactionNotifier |
| Private members | _camelCase |
_handleSubmit |
Feature Structure Template
When adding a new feature, follow this pattern:
features/
└── <feature_name>/
├── provider.dart # Riverpod providers for this feature
├── screen.dart # Top-level screen widget
└── widgets/ # Sub-widgets (create only if needed)
Key Patterns
Error handling — use Result<T>:
final result = await repository.getAccounts();
result.when(
success: (accounts) => ...,
failure: (error) => ...,
);
Localization — use AppStrings from core/l10n/app_strings.dart, never hardcode strings visible to the user.
Currency formatting — use CurrencyUtils and amountFormatProvider, never format amounts manually.
Haptics — route all haptic feedback through HapticService, never call HapticFeedback directly.
Colors for accounts — use CardColorService, not hardcoded colors.
Feature gating — wrap gated UI with PaywallGuard:
class ExportScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final flags = ref.watch(featureFlagsProvider);
return PaywallGuard(
canAccess: flags.canExportCsv,
child: ExportContent(),
);
}
}
Quantity limits in repositories:
Future<Result<void>> createAccount(Account account) async {
final flags = ref.read(featureFlagsProvider);
final count = await _db.countAccounts();
if (flags.maxAccounts != -1 && count >= flags.maxAccounts) {
return Result.failure(FeatureLimitException());
}
return Result.success(await _db.insertAccount(account));
}
VIP-only routes — use GoRouter redirect, never guard inside the screen itself:
GoRoute(
path: '/analytics',
redirect: (context, state) {
final flags = ref.read(featureFlagsProvider);
return flags.canSeeAnalytics ? null : '/paywall';
},
builder: (context, state) => AnalyticsScreen(),
),
Existing Features
- dashboard — main screen with account carousel (
BalanceCard), transaction list, search, filter chips, budget progress, account editor overlay - add_transaction — form screen: amount input, type toggle (income/expense), category picker, currency picker, account selector, date/time pickers, note field
- categories — category list management
- settings — theme, language, currency, budget, amount format, card text color, haptics, currency conversions
What NOT to Do
- Do not add comments
- Do not put business logic in screen or widget files
- Do not access
AppDatabasedirectly from features — use repositories - Do not create new providers in
shared/providers/unless the provider is needed by 2+ features - Do not use
BuildContextacross async gaps without checkingmounted - Do not hardcode user-facing strings — use
AppStrings - Do not format currency amounts manually — use
CurrencyUtils - Do not check
user.isViporplan == UserPlan.vipin widgets or screens — usefeatureFlagsProvider - Do not enforce feature limits in widgets — put them in repositories and throw
FeatureLimitException - Do not add a new gated feature without adding a corresponding getter to
FeatureFlagsand implementing it in bothFreeFeatureFlagsandVipFeatureFlags