Files
2026-05-27 17:01:13 +03:00

219 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План разработки 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`**: возвращать или нет.