Describe ProfileService, profileProvider, ProfileScreen sections (avatar, body params, goal/activity, daily calories, logout), EditProfileSheet, and HomeScreen name integration. No new backend endpoints needed — GET/PUT /profile already exist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
12 KiB
Итерация 6: Экран профиля
Цель: заменить заглушку «Раздел в разработке» полноценным экраном профиля — показать данные пользователя, дать возможность редактировать параметры тела и цели, отобразить рассчитанную норму калорий и выйти из аккаунта.
Зависимости: итерация 0 (авторизация, модель User, GET /profile, PUT /profile).
Структура задач
6.1 Flutter: ProfileService
└── GET /profile, PUT /profile
6.2 Flutter: profileProvider
└── StateNotifier, load(), update()
6.3 Flutter: ProfileScreen
├── 6.3.1 Секция аватара и имени
├── 6.3.2 Секция параметров тела
├── 6.3.3 Секция цели и активности
├── 6.3.4 Секция нормы калорий
└── 6.3.5 Кнопка выхода
6.4 Flutter: EditProfileSheet
└── Форма редактирования всех параметров
6.5 Flutter: HomeScreen — имя из профиля
6.1 Backend: уже реализовано
Новых эндпоинтов не требуется. Оба маршрута существуют:
GET /profile — возвращает полный профиль пользователя
PUT /profile — частичное обновление; если изменяются height/weight/age/gender/activity/goal,
daily_calories пересчитывается автоматически (Mifflin-St Jeor)
GET /profile — ответ
{
"id": "uuid",
"email": "user@example.com",
"name": "Алексей",
"avatar_url": null,
"height_cm": 175,
"weight_kg": 72.0,
"age": 28,
"gender": "male",
"activity": "moderate",
"goal": "maintain",
"daily_calories": 2480,
"plan": "free",
"preferences": {}
}
PUT /profile — запрос (все поля опциональны)
{
"name": "Алексей",
"height_cm": 175,
"weight_kg": 72.0,
"age": 28,
"gender": "male",
"activity": "moderate",
"goal": "maintain"
}
Допустимые значения:
gender:"male"|"female"activity:"low"|"moderate"|"high"goal:"lose"|"maintain"|"gain"
6.2 Flutter: ProfileService
class ProfileService {
ProfileService(this._client);
final ApiClient _client;
Future<User> getProfile() async {
final json = await _client.get('/profile');
return User.fromJson(json);
}
Future<User> updateProfile(UpdateProfileRequest req) async {
final json = await _client.put('/profile', data: req.toJson());
return User.fromJson(json);
}
}
class UpdateProfileRequest {
final String? name;
final int? heightCm;
final double? weightKg;
final int? age;
final String? gender;
final String? activity;
final String? goal;
}
6.3 Flutter: profileProvider
final profileProvider =
StateNotifierProvider<ProfileNotifier, AsyncValue<User>>(
(ref) => ProfileNotifier(ref.read(profileServiceProvider)),
);
class ProfileNotifier extends StateNotifier<AsyncValue<User>> {
ProfileNotifier(this._service) : super(const AsyncValue.loading()) {
load();
}
Future<void> load() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => _service.getProfile());
}
Future<bool> update(UpdateProfileRequest req) async {
try {
final updated = await _service.updateProfile(req);
state = AsyncValue.data(updated);
return true;
} catch (_) {
return false;
}
}
}
6.4 Flutter: ProfileScreen
Макет
┌─────────────────────────────────────┐
│ Профиль Изменить │ ← AppBar
├─────────────────────────────────────┤
│ │
│ [○] Алексей │ ← аватар-заглушка + имя
│ user@example.com │
│ │
├─────────────────────────────────────┤
│ ПАРАМЕТРЫ ТЕЛА │
│ Рост 175 см › │
│ Вес 72 кг › │
│ Возраст 28 лет › │
│ Пол Мужской › │
├─────────────────────────────────────┤
│ ЦЕЛЬ И АКТИВНОСТЬ │
│ Цель Поддержание › │
│ Активность Средняя › │
├─────────────────────────────────────┤
│ ПИТАНИЕ │
│ Норма калорий 2 480 ккал │
│ Рассчитано по параметрам тела │
├─────────────────────────────────────┤
│ │
│ [ Выйти из аккаунта ] │
│ │
└─────────────────────────────────────┘
6.3.1 Секция аватара и имени
- Круглый контейнер с инициалами (первая буква имени) на фоне
AppColors.primary.withValues(alpha: 0.15)— еслиavatar_urlравен null - Под контейнером: имя (
titleMedium) и email (bodySmall,textSecondary)
6.3.2 Секция параметров тела
Сгруппированная карточка (Card) с ListTile-строками в стиле iOS:
- Рост —
${height} смили «Не задано» серым - Вес —
${weight} кгили «Не задано» - Возраст —
${age} летили «Не задано» - Пол — «Мужской» / «Женский» / «Не задано»
Строки — только для чтения. Редактирование открывается кнопкой Изменить в AppBar.
Если все параметры не заданы — показать inline-баннер:
«Укажите параметры тела для расчёта нормы калорий»
6.3.3 Секция цели и активности
Аналогичная карточка:
- Цель: «Похудение» | «Поддержание» | «Набор массы»
- Активность: «Низкая» | «Средняя» | «Высокая»
6.3.4 Секция нормы калорий
Карточка с одной строкой:
- Если
daily_calories != null— показать «N ккал/день» - Иначе — «Задайте параметры тела»
Подпись: «Рассчитано автоматически по формуле Миффлина-Сан Жеора».
6.3.5 Кнопка выхода
OutlinedButton в цвете AppColors.error, по всей ширине.
Перед выходом — AlertDialog с подтверждением.
По подтверждению: ref.read(authProvider.notifier).signOut().
6.5 Flutter: EditProfileSheet
Открывается из кнопки «Изменить» в AppBar — showModalBottomSheet с прокрутой.
Поля формы
| Поле | Тип ввода | Валидация |
|---|---|---|
| Имя | TextFormField | не пустое |
| Рост | TextFormField (число) | 100–250 |
| Вес | TextFormField (десятичное) | 30–300 |
| Возраст | TextFormField (число) | 10–120 |
| Пол | SegmentedButton: «Мужской» / «Женский» | — |
| Цель | SegmentedButton: «Похудение» / «Поддержание» / «Набор» | — |
| Активность | SegmentedButton: «Низкая» / «Средняя» / «Высокая» | — |
Поведение
- Поля предзаполнены текущими значениями из
profileProvider - Кнопка «Сохранить» (FilledButton) — вызывает
profileProvider.notifier.update(...) - При успехе — закрывает sheet, показывает
SnackBar«Профиль обновлён» - При ошибке — показывает
SnackBar«Не удалось сохранить» - Калории пересчитываются на сервере автоматически — UI обновится из ответа
Макет sheet
┌─────────────────────────────────────┐
│ ── (drag handle) │
│ Редактировать профиль │
├─────────────────────────────────────┤
│ Имя │
│ [Алексей ] │
│ │
│ Рост (см) Вес (кг) │
│ [175 ] [72.0 ] │
│ │
│ Возраст (лет) │
│ [28 ] │
│ │
│ Пол │
│ [ Мужской ● | Женский ] │
│ │
│ Цель │
│ [ Похудение | Поддержание● | Набор ]│
│ │
│ Активность │
│ [ Низкая | Средняя● | Высокая ] │
│ │
│ [ Сохранить ] │
└─────────────────────────────────────┘
6.6 Flutter: HomeScreen — имя пользователя
Приветствие на главном экране («Доброе утро, Алексей!») сейчас использует статичное имя или имя из authProvider.
После этой итерации: имя берётся из profileProvider:
final user = ref.watch(profileProvider).valueOrNull;
final name = user?.name ?? '';
Оценка нагрузки
| Действие | OpenAI | БД запросов |
|---|---|---|
| Открытие вкладки Профиль | 0 | 1 (SELECT users) |
| Сохранение профиля | 0 | 1–2 (UPDATE + SELECT) |
Экран профиля не тратит AI-квоту.
Результат итерации
- Пользователь видит своё имя, параметры тела и цели
- Может обновить параметры — норма калорий пересчитывается мгновенно
- Может выйти из аккаунта
- Приветствие на главном экране показывает реальное имя