# План разработки 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`. ## Статус каркаса ### Готово - [x] Структура каталогов, все 5 фич имеют `domain/data/application`. - [x] Drift: таблицы, DAO с `watch*`-методами, конвертеры enum, миграция onCreate. - [x] `AppDatabase` и `appDatabaseProvider` (keepAlive). - [x] Доменные сущности (User/Settings/Account/Category/Transaction) + мапперы + impl-репозитории. - [x] Контроллеры: `usersController`, `activeUserController`, `accountsController`, `categoriesController`, `transactionsController`, `settingsController`. - [x] Roвтинг (`StatefulShellRoute` + 4 таба), `AppScaffold`, `AppBottomNav`. - [x] Тема (`Palette` extension, light/dark), `themeModeController` (in-memory). - [x] Локализация ru/en (ARB → `flutter gen-l10n` → `lib/l10n/`), `context.l10n`. - [x] Главный экран (`HomeScreen`): MonthHeader, AccountTabs, MonthKpiCard, CategoryDonutCard, TransactionsSection с группировкой по дням, FAB (no-op). ### НЕ готово (приоритет сверху вниз) 1. ~~**Переключить Home с моков на DAO-стримы.**~~ ✅ - `_mock_data.dart` удалён. - `iconForCategory` + `colorForCategory` → `features/categories/presentation/widgets/category_icon.dart`. - `iconForAccount` + `shortAccountLabel` → `features/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.createUser` → `activeUserController.setActiveUser`). Маршрут `/onboarding` — отдельный top-level GoRoute. - В `appRouter` добавлен `redirect`-callback + `refreshListenable` на `activeUserControllerProvider`: пока пользователь не задан — редирект на `/onboarding`; после задания — обратно на `/home`. - При создании пользователя через `usersController.createUser` запускается `UserSeeder.seedForNewUser(userId)` ([user_seeder.dart](lib/src/features/user/application/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`**: возвращать или нет.