17 KiB
План разработки 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/). - Тема: свой
Paletteextension (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-контроллеры (
@riverpodNotifier/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. - Тема (
Paletteextension, light/dark),themeModeController(in-memory). - Локализация ru/en (ARB →
flutter gen-l10n→lib/l10n/),context.l10n. - Главный экран (
HomeScreen): MonthHeader, AccountTabs, MonthKpiCard, CategoryDonutCard, TransactionsSection с группировкой по дням, FAB (no-op).
НЕ готово (приоритет сверху вниз)
-
Переключить 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через конструктор и читают данные через.valueAsyncValue (во время первичной загрузки возвращается пустой список).
-
Активный пользователь во всём 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), который засевает дефолтные счета (Карта/Наличные/Копилка), категории (Продукты/Жильё/Транспорт/Кафе/Досуг/Зарплата) и демонстрационные транзакции. Демо-транзакции (_seedDemoTransactions) — временные, удалить после п.3 (Add Transaction UI).
-
Add/Edit Transaction.
- FAB на Home сейчас
onPressed: () {}. Маршрут/transactions/new+ bottom-sheet или экран: выбор счёта, типа, категории, суммы, даты, заметки. На сабмит —transactionsController.createTransaction.
- FAB на Home сейчас
-
Экраны фич:
- Accounts (
AccountsScreenсейчас placeholder): список черезaccountsStream, баланс черезaccountBalance, CRUD черезaccountsController. - Analytics: графики по
fl_chart(расходы/доходы по месяцам, по категориям). Источник — те же стримы транзакций + агрегация. - Profile: список профилей (
usersStream), переключение активного, переименование/удаление, настройки (валюта, локаль, первый день месяца) черезsettingsController.
- Accounts (
-
Settings ↔ Theme/Locale.
- Сейчас
themeModeControllerхранит ThemeMode в памяти. Подменить на чтение/запись черезsettingsController(поляthemeMode,locale) для активного пользователя. MaterialApp.routerдолжен братьlocaleиз настроек.
- Сейчас
-
Переводы между счетами.
- В
Transaction.transferToAccountIdполе есть. UI и контроллерcreateTransactionпринимают, но баланс счёта-получателя пока не учитывает их вMonthSummary(см.case transfer: break;). Решить, как считать: одна запись сtransferTovs парные транзакции (см. развилку ниже).
- В
-
Прочее:
shared/formatters/пуст — добавитьintl-форматтер денег (используетcurrencyиз настроек) и дат, на который перейдутMoneyTextи группировка дней вhome_screen.dart.- Очистить ненужный re-export enum'ов из
enum_converters.dart, либо явно задокументировать паттерн «enum живёт рядом с Drift-таблицей, импортируется через конвертер». - Тесты: ни одного теста не написано. Минимум — unit на репозитории через
AppDatabase.forTesting.
Команды
flutter pub getdart run build_runner build --delete-conflicting-outputs— после изменения@DriftDatabase,@riverpod,@freezed, ARB-файлов локализации.flutter analyzeflutter run(Android-эмулятор).
Открытые развилки (не принимать решение без согласования)
- ID:
→ решено:int autoIncrementUUID v4 / text(schemaVersion=2). Готово к облачной синхронизации. - Переводы между счетами: одна запись с
transferToAccountIdvs парные транзакции (income на одном счёте + expense на другом). Текущая модель — первое; агрегаты их игнорируют. - Агрегаты для аналитики: считать на клиенте в Provider (сейчас) vs SQL-агрегаты в DAO
(
watchAccountBalance,watchTotalsByCategory(period)). Перейти на SQL при росте объёма данных. riverpod_lint/custom_lint: возвращать или нет.