Files
food-ai/docs/plans/Iteration_6.md
dbastrikin 79c32f226c docs: add Iteration 6 — profile screen
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>
2026-02-22 15:59:09 +02:00

12 KiB
Raw Permalink Blame History

Итерация 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 (число) 100250
Вес TextFormField (десятичное) 30300
Возраст TextFormField (число) 10120
Пол 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 12 (UPDATE + SELECT)

Экран профиля не тратит AI-квоту.


Результат итерации

  • Пользователь видит своё имя, параметры тела и цели
  • Может обновить параметры — норма калорий пересчитывается мгновенно
  • Может выйти из аккаунта
  • Приветствие на главном экране показывает реальное имя