7.3 KiB
NewBudget — Flutter personal finance app
Commands
flutter pub get
dart run build_runner build --delete-conflicting-outputs # after changing @riverpod / @DriftDatabase / @freezed / ARB
flutter gen-l10n # regenerate localizations (also runs with pub get)
flutter analyze
flutter run # Android emulator
flutter test
Run
build_runnerwhenever you touch any.dartfile that has@riverpod,@DriftDatabase,@freezed, or@JsonSerializableannotations, or after editinglib/l10n/*.arb.
Stack
| Concern | Library |
|---|---|
| State | flutter_riverpod + riverpod_annotation + riverpod_generator (code-gen) |
| Database | drift + drift_flutter (SQLite, reactive streams) |
| Navigation | go_router v17 — StatefulShellRoute.indexedStack (4 bottom-tab branches) |
| Entities | freezed_annotation (immutable, copyWith, ==) |
| Localization | flutter_localizations + ARB → flutter gen-l10n → lib/l10n/ |
| Charts | fl_chart |
| Fonts | google_fonts |
NOT used: shared_preferences, riverpod_lint/custom_lint (intentionally omitted).
Architecture (feature-first, layered — do not break)
presentation → application → domain ← data
(UI) (Riverpod) (pure Dart) (Drift impl)
- presentation — Widgets only. Uses
ref.watch(...), calls controller methods. No Drift imports. - application —
@riverpodNotifier/AsyncNotifier controllers. Validation, orchestration, UI state. Depends only on domain abstractions. - domain — pure-Dart entities + abstract repository interfaces. Zero Flutter/Drift deps.
- data — Drift tables, DAOs, mappers (row ↔ entity), repository implementations. Only layer that touches SQL.
No use-case classes — controllers call repositories directly.
Key directory map
lib/
main.dart # runApp(ProviderScope(child: NewBudgetApp()))
l10n/ # generated: app_localizations*.dart
src/
app/
app.dart # MaterialApp.router, theme, locale
l10n/l10n.dart # context.l10n extension
router/app_router.dart # GoRouter + StatefulShellRoute (4 tabs)
router/app_routes.dart # route path constants
theme/app_theme.dart # light/dark ThemeData
theme/app_colors.dart # Palette extension (paper/ink/line/accent/positive/negative)
theme/theme_mode_controller.dart # @Riverpod(keepAlive) ThemeMode — in-memory for now
core/
database/app_database.dart # @DriftDatabase, schemaVersion=2
database/tables/ # users / app_preferences / settings / accounts / categories / transactions
database/daos/ # *_dao.dart with .watch*() methods
database/converters/enum_converters.dart # TypeConverter + re-exports all enums (UI imports enums from here)
providers/database_provider.dart # @Riverpod(keepAlive) AppDatabase
money/money.dart # amounts stored as int minor units (kopecks/cents)
features/
user/ # domain + data + application ready; presentation empty
settings/ # domain + data + application ready; presentation empty
accounts/ # domain + data + application ready; presentation placeholder
categories/ # domain + data + application ready; presentation empty
transactions/# domain + data + application ready; presentation empty
home/presentation/
screens/home_screen.dart # ConsumerWidget, assembles widgets below
widgets/ # MonthHeader, AccountTabs, MonthKpiCard, CategoryDonutCard,
# TransactionsSection, DayHeader, TxRow, MoneyText, FabAddTransaction
state/selected_category_filter.dart # account/category filter providers
month_summary.dart # client-side aggregates for KPI / donut
_mock_data.dart # TEMPORARY mock providers + icon mappers (to be deleted)
analytics/ # placeholder
profile/ # placeholder (theme switcher only)
shared/
widgets/app_scaffold.dart # StatefulShellRoute wrapper + AppBottomNav
formatters/ # EMPTY — planned intl money/date formatters
Data model
All amounts: int minor units. All IDs: String UUID v4 (client-generated, cloud-sync ready).
Every domain table has a userId FK → users.
| Table | Key fields |
|---|---|
users |
id, name, createdAt |
app_preferences |
key (PK), value — stores active_user_id |
settings |
userId FK, baseCurrency, themeMode(enum), locale, firstDayOfMonth |
accounts |
id, userId, name, type(enum), currency, initialBalance(int), iconCode, colorValue, archived |
categories |
id, userId, name, type(enum), iconCode, colorValue, parentId(nullable), archived |
transactions |
id, userId, accountId, categoryId(nullable), type(enum), amount(int), date, note(nullable), transferToAccountId(nullable), createdAt |
Enums live alongside their Drift tables; enum_converters.dart is the single import point for UI.
Code-gen gotchas
- Generated files:
*.g.dart(Riverpod/Drift/JSON),*.freezed.dart. Both are excluded from analysis but must be committed. - After any schema change to
@DriftDatabaseor table files, re-runbuild_runnerand bumpschemaVersioninapp_database.dart. - Riverpod
@riverpodproviders generate into the same*.g.dart— don't split provider + its generated file across separatepartdirectives in unexpected ways.
Localization
ARB files in lib/l10n/app_en.arb and lib/l10n/app_ru.arb.
Access strings via context.l10n.someKey (extension from src/app/l10n/l10n.dart).
After editing ARB files run flutter gen-l10n (or flutter pub get).
Theme / colors
Use Theme.of(context).extension<Palette>()! for brand colors.
Palette tokens: paper, ink, line, accent, positive, negative.
Do not use hard-coded color constants in widgets.
Active user
Active profile is stored in app_preferences table with key active_user_id.
Read via activeUserControllerProvider. Currently home screen uses a hard-coded mock userId —
transitioning to the real provider is priority #1 (see PLAN.md).
What's left (priority order)
- Replace
_mock_data.dartmock providers with real DAO stream providers in Home widgets - Wire
activeUserControllerProvideruserId throughout all feature streams - Add/Edit Transaction screen (FAB →
/transactions/new) - Accounts, Analytics, Profile screens
- Persist theme/locale via
settingsController(replace in-memorythemeModeController) - Transfer transactions: fix balance aggregation (
case transfer: break;inmonth_summary.dart) shared/formatters/— intl money + date formatters- Unit tests (start with repository layer using
AppDatabase.forTesting())
Open decisions (discuss before implementing)
- Transfer model: single record with
transferToAccountIdvs paired income+expense records - Aggregates: client-side Provider (current) vs SQL
watchTotalsByCategory(period)in DAO riverpod_lint/custom_lint: re-add or keep omitted