219 lines
17 KiB
Markdown
219 lines
17 KiB
Markdown
# План разработки 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`**: возвращать или нет.
|