Files
OnBudget/CLAUDE.md
T
2026-05-27 17:01:17 +03:00

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_runner whenever you touch any .dart file that has @riverpod, @DriftDatabase, @freezed, or @JsonSerializable annotations, or after editing lib/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-l10nlib/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@riverpod Notifier/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 @DriftDatabase or table files, re-run build_runner and bump schemaVersion in app_database.dart.
  • Riverpod @riverpod providers generate into the same *.g.dart — don't split provider + its generated file across separate part directives 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)

  1. Replace _mock_data.dart mock providers with real DAO stream providers in Home widgets
  2. Wire activeUserControllerProvider userId throughout all feature streams
  3. Add/Edit Transaction screen (FAB → /transactions/new)
  4. Accounts, Analytics, Profile screens
  5. Persist theme/locale via settingsController (replace in-memory themeModeController)
  6. Transfer transactions: fix balance aggregation (case transfer: break; in month_summary.dart)
  7. shared/formatters/ — intl money + date formatters
  8. Unit tests (start with repository layer using AppDatabase.forTesting())

Open decisions (discuss before implementing)

  • Transfer model: single record with transferToAccountId vs paired income+expense records
  • Aggregates: client-side Provider (current) vs SQL watchTotalsByCategory(period) in DAO
  • riverpod_lint/custom_lint: re-add or keep omitted