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

17 KiB
Raw Blame History

План разработки Flutter-приложения для учёта личных финансов (Android)

Контекст

Каркас приложения собран и запускается (C:\Sanders\Flutter\NewBudget, Flutter 3.35.1 / Dart 3.9.0). Главный экран отрисовывается на мок-данных — следующий этап работы переключить UI на реальные стримы из Drift и закрыть оставшиеся экраны.

Зафиксированные технические решения:

  • БД: Drift (SQLite), реляционные связи account→transaction→category, реактивные стримы.
  • State: Riverpod с кодогенерацией (@riverpod + riverpod_generator + build_runner).
  • Навигация: go_router + StatefulShellRoute.indexedStack (нижний таб-бар на 4 ветки).
  • Локализация: flutter_localizations + ARB через flutter gen-l10n (см. lib/l10n/).
  • Тема: свой Palette extension (app_colors.dart) поверх Theme.of(context).
  • Пользователи: несколько локальных профилей; все доменные таблицы ссылаются на userId.
  • Активный пользователь: key-value таблица app_preferences (settings_table.dart), а не shared_preferences.

Архитектура: разделение UI и логики (НЕ МЕНЯТЬ)

Каркас строится по feature-first с явными слоями внутри каждой фичи. Зависимости направлены строго внутрь (presentation → application → domain ← data), внешние слои не знают о Drift:

presentation  →  application  →  domain  ←  data
   (UI)            (логика)       (контракты)   (Drift+реализации)
  • presentation — только Widgets/экраны. Читает состояние через ref.watch(...), вызывает методы контроллеров. Не содержит бизнес-логики, не знает про Drift, не делает запросов к БД.
  • application — Riverpod-контроллеры (@riverpod Notifier/AsyncNotifier). Здесь живёт логика: валидация, оркестрация вызовов репозиториев, формирование состояния для UI. Зависит только от абстракций domain.
  • domain — чистый Dart: сущности (User, Account, ...) и абстрактные интерфейсы репозиториев. Никаких зависимостей от Flutter/Drift. Это контракт между логикой и данными.
  • data — реализации репозиториев + Drift (таблицы, DAO, мапперы row↔entity). Только здесь пишется SQL.

Слой usecase-классов сознательно опускаем — контроллеры вызывают репозитории напрямую.

Структура каталогов (фактическая)

lib/
  main.dart                                       # runApp(ProviderScope(child: NewBudgetApp()))
  l10n/                                            # сгенерированные AppLocalizations (flutter gen-l10n)
  src/
    app/
      app.dart                                    # MaterialApp.router, тема, локализация
      l10n/l10n.dart                              # context.l10n extension + re-export
      router/
        app_router.dart                           # GoRouter, StatefulShellRoute (4 ветки)
        app_routes.dart                           # константы путей
      theme/
        app_theme.dart                            # light/dark ThemeData
        app_colors.dart                           # Palette extension (paper/ink/line/accent/positive/negative)
        theme_mode_controller.dart                # @Riverpod(keepAlive) ThemeMode — пока in-memory
    core/
      database/
        app_database.dart                         # @DriftDatabase, schemaVersion=1
        tables/                                   # users / settings / accounts / categories / transactions
                                                  #  + AppPreferencesTable (key-value)
        daos/                                     # *_dao.dart с .watch()-методами
        converters/enum_converters.dart           # TypeConverter для enum + ре-экспорт enum-ов
      providers/database_provider.dart            # @Riverpod(keepAlive) AppDatabase
      money/money.dart                            # хранение в минорных единицах (int)
      errors/failures.dart
      constants/
    features/
      user/         (domain / data / application — реализованы; presentation пуст)
        application/
          user_providers.dart                     # provider репозитория (DI)
          users_controller.dart                   # CRUD профилей
          active_user_controller.dart             # текущий активный профиль (через app_preferences)
      settings/     (domain / data / application — реализованы; presentation пуст)
      accounts/     (domain / data / application — реализованы; presentation — placeholder)
      categories/   (domain / data / application — реализованы; presentation пуст)
      transactions/ (domain / data / application — реализованы; presentation пуст)
      home/         presentation/
        screens/home_screen.dart                  # ConsumerWidget, собирает виджеты ниже
        widgets/                                  # MonthHeader, AccountTabs, MonthKpiCard, CategoryDonutCard,
                                                  #  TransactionsSection, DayHeader, TxRow, MoneyText, FabAddTransaction
        state/selected_category_filter.dart       # фильтры (account/category) — @riverpod
        month_summary.dart                        # агрегаты для KPI/донат — СЧИТАЮТСЯ НА КЛИЕНТЕ
        _mock_data.dart                           # ВРЕМЕННЫЕ моки + iconForCategory/iconForAccount
      analytics/    presentation/screens/         # placeholder
      profile/      presentation/screens/         # переключатель темы + placeholder
    shared/
      widgets/
        app_scaffold.dart                         # обёртка StatefulShellRoute + AppBottomNav
        app_bottom_nav.dart
        placeholder_screen.dart
      formatters/                                  # пусто (планируется intl-форматирование)

Модель данных (Drift, schemaVersion=2)

Деньги хранятся как целые минорные единицы (копейки/центы) в int. Идентификаторы — String UUID v4 (генерируются на клиенте, готовы к облачной синхронизации). Все доменные таблицы имеют userId (FK → users).

  • users: id, name, createdAt.
  • app_preferences: key (PK), value. Хранит, в частности, active_user_id.
  • settings (на пользователя): userId (FK), baseCurrency, themeMode (enum), locale, firstDayOfMonth.
  • accounts: id, userId (FK), name, type (enum: cash/card/bank/savings), currency, initialBalance (int), iconCode, colorValue, archived, createdAt.
  • categories: id, userId (FK), name, type (enum: income/expense), iconCode, colorValue, parentId (nullable), archived.
  • transactions: id, userId (FK), accountId (FK), categoryId (FK, nullable), type (enum: income/expense/transfer), amount (int, минорные единицы), date, note (nullable), transferToAccountId (nullable), createdAt.

Чистые сущности в domain/entities (immutable, freezed), мапперы в data/mappers, enum'ы — через Drift TypeConverter в core/database/converters/enum_converters.dart (этот файл также реэкспортирует enum'ы — UI берёт их оттуда, не из Drift-таблиц).

DAO предоставляют реактивные методы (Stream через .watch()): watchByUser, watchTransactions(filter), watchAccountBalance(accountId). Сложные агрегаты по категориям/периоду пока считаются на клиенте (см. home/presentation/month_summary.dart) — позже стоит вынести в SQL-агрегаты DAO.

Зависимости (фактические в pubspec.yaml)

Runtime: flutter_riverpod, riverpod_annotation, drift, drift_flutter, go_router, intl, freezed_annotation, json_annotation, google_fonts, fl_chart.

Dev: build_runner, riverpod_generator, drift_dev, freezed, json_serializable, flutter_lints.

riverpod_lint / custom_lint НЕ подключены (отступление от исходного плана). Если решим вернуть — добавить в dev_deps и подключить custom_lint в analysis_options.yaml.

Статус каркаса

Готово

  • Структура каталогов, все 5 фич имеют domain/data/application.
  • Drift: таблицы, DAO с watch*-методами, конвертеры enum, миграция onCreate.
  • AppDatabase и appDatabaseProvider (keepAlive).
  • Доменные сущности (User/Settings/Account/Category/Transaction) + мапперы + impl-репозитории.
  • Контроллеры: usersController, activeUserController, accountsController, categoriesController, transactionsController, settingsController.
  • Roвтинг (StatefulShellRoute + 4 таба), AppScaffold, AppBottomNav.
  • Тема (Palette extension, light/dark), themeModeController (in-memory).
  • Локализация ru/en (ARB → flutter gen-l10nlib/l10n/), context.l10n.
  • Главный экран (HomeScreen): MonthHeader, AccountTabs, MonthKpiCard, CategoryDonutCard, TransactionsSection с группировкой по дням, FAB (no-op).

НЕ готово (приоритет сверху вниз)

  1. Переключить Home с моков на DAO-стримы.

    • _mock_data.dart удалён.
    • iconForCategory + colorForCategoryfeatures/categories/presentation/widgets/category_icon.dart.
    • iconForAccount + shortAccountLabelfeatures/accounts/presentation/widgets/account_icon.dart.
    • kAllAccountsId теперь живёт в home/presentation/state/selected_category_filter.dart.
    • month_summary.dart пересажен на accountsStreamProvider(userId) и transactionsStreamProvider(userId); filteredTransactionsProvider и monthSummaryProvider@riverpod с параметром userId.
    • Все виджеты Home (HomeScreen, AccountTabs, MonthKpiCard, CategoryDonutCard, TransactionsSectionHeader, CategoryFilterPill) принимают userId через конструктор и читают данные через .value AsyncValue (во время первичной загрузки возвращается пустой список).
  2. Активный пользователь во всём UI.

    • HomeScreen тянет активного пользователя из activeUserControllerProvider и прокидывает user.id во все дочерние виджеты.
    • Onboarding: features/user/presentation/screens/onboarding_screen.dart (поле имени → usersController.createUseractiveUserController.setActiveUser). Маршрут /onboarding — отдельный top-level GoRoute.
    • В appRouter добавлен redirect-callback + refreshListenable на activeUserControllerProvider: пока пользователь не задан — редирект на /onboarding; после задания — обратно на /home.
    • При создании пользователя через usersController.createUser запускается UserSeeder.seedForNewUser(userId) (user_seeder.dart), который засевает дефолтные счета (Карта/Наличные/Копилка), категории (Продукты/Жильё/Транспорт/Кафе/Досуг/Зарплата) и демонстрационные транзакции. Демо-транзакции (_seedDemoTransactions) — временные, удалить после п.3 (Add Transaction UI).
  3. Add/Edit Transaction.

    • FAB на Home сейчас onPressed: () {}. Маршрут /transactions/new + bottom-sheet или экран: выбор счёта, типа, категории, суммы, даты, заметки. На сабмит — transactionsController.createTransaction.
  4. Экраны фич:

    • Accounts (AccountsScreen сейчас placeholder): список через accountsStream, баланс через accountBalance, CRUD через accountsController.
    • Analytics: графики по fl_chart (расходы/доходы по месяцам, по категориям). Источник — те же стримы транзакций + агрегация.
    • Profile: список профилей (usersStream), переключение активного, переименование/удаление, настройки (валюта, локаль, первый день месяца) через settingsController.
  5. Settings ↔ Theme/Locale.

    • Сейчас themeModeController хранит ThemeMode в памяти. Подменить на чтение/запись через settingsController (поля themeMode, locale) для активного пользователя.
    • MaterialApp.router должен брать locale из настроек.
  6. Переводы между счетами.

    • В Transaction.transferToAccountId поле есть. UI и контроллер createTransaction принимают, но баланс счёта-получателя пока не учитывает их в MonthSummary (см. case transfer: break;). Решить, как считать: одна запись с transferTo vs парные транзакции (см. развилку ниже).
  7. Прочее:

    • shared/formatters/ пуст — добавить intl-форматтер денег (использует currency из настроек) и дат, на который перейдут MoneyText и группировка дней в home_screen.dart.
    • Очистить ненужный re-export enum'ов из enum_converters.dart, либо явно задокументировать паттерн «enum живёт рядом с Drift-таблицей, импортируется через конвертер».
    • Тесты: ни одного теста не написано. Минимум — unit на репозитории через AppDatabase.forTesting.

Команды

  • flutter pub get
  • dart run build_runner build --delete-conflicting-outputs — после изменения @DriftDatabase, @riverpod, @freezed, ARB-файлов локализации.
  • flutter analyze
  • flutter run (Android-эмулятор).

Открытые развилки (не принимать решение без согласования)

  • ID: int autoIncrement → решено: UUID v4 / text (schemaVersion=2). Готово к облачной синхронизации.
  • Переводы между счетами: одна запись с transferToAccountId vs парные транзакции (income на одном счёте + expense на другом). Текущая модель — первое; агрегаты их игнорируют.
  • Агрегаты для аналитики: считать на клиенте в Provider (сейчас) vs SQL-агрегаты в DAO (watchAccountBalance, watchTotalsByCategory(period)). Перейти на SQL при росте объёма данных.
  • riverpod_lint/custom_lint: возвращать или нет.