Магазин за один конкурсный день
Зачем это нужно
Пять предыдущих глав разбирали запросы по одному. Эта — про то, как собрать из них работающее приложение за день, и в каком порядке это делать, чтобы к вечеру было что показать.
Порядок важнее, чем кажется. Один и тот же объём работы, сделанный в разной последовательности, даёт либо приложение, где работает каталог, вход и корзина, либо набор аккуратных классов, из которых не складывается ни один живой экран.
Что вообще требуется
В спецификации задания ровно четырнадцать операций по шести коллекциям. Вот они целиком — этот список стоит держать перед глазами:
| № | Метод и путь | Что это | Токен |
|---|---|---|---|
| 1 | POST /collections/users/records | Регистрация | нет |
| 2 | POST /collections/users/auth-with-password | Вход | нет |
| 3 | GET /collections/users/records/{id} | Профиль | да |
| 4 | PATCH /collections/users/records/{id} | Изменение профиля | да |
| 5 | GET /collections/news/records | Акции и новости | да |
| 6 | GET /collections/products/records | Каталог, он же поиск через filter | да |
| 7 | GET /collections/products/records/{id} | Карточка товара | да |
| 8 | POST /collections/cart/records | Добавить в корзину | да |
| 9 | PATCH /collections/cart/records/{id} | Изменить количество | да |
| 10 | POST /collections/orders/records | Оформить заказ | да |
| 11 | GET /collections/project/records | Список проектов | да |
| 12 | POST /collections/project/records | Создать проект с файлом | да |
| 13 | GET /collections/_authOrigins/records | Список сессий | да |
| 14 | DELETE /collections/_authOrigins/records/{id} | Удалить запись сессии | да |
Поиск — не отдельный запрос, а тот же шестой с параметром filter. А удаления из корзины в списке нет, хотя приложению оно необходимо: иначе товар не убрать, а после заказа корзина останется полной. PocketBase его поддерживает — просто в спецификацию оно не попало.
Столбец «токен» стоит прочитать внимательно: без токена работают только два запроса — регистрация и вход. Всё остальное требует заголовка Authorization, и это определяет порядок работ.
Порядок, который окупается
Шаг 1. Каркас сети, ~20 минут. Зависимости, разрешение INTERNET в манифесте, Retrofit.Builder с конвертером, один-единственный метод — каталог. Запустить, убедиться, что список товаров приходит и разбирается. Пока нет ни экранов, ни архитектуры.
Шаг 2. Вход, ~40 минут. Регистрация, вход, сохранение токена в DataStore, интерцептор, который его вешает. Проверить, что профиль читается.
Шаг 3. Каталог, ~час. Экран списка, карточка товара, поиск. Здесь появляются состояния: загрузка, содержимое, пусто, ошибка.
Шаг 4. Корзина и заказ, ~час. Добавление, количество, сборка корзины с ценами, оформление.
Дальше — по остатку времени: акции с баннерами, профиль и его редактирование, проекты с загрузкой файла, обработка отказов и поведение без сети.
Вход делается раньше каталога, хотя каталог интереснее. Причина простая: без токена двенадцать запросов из четырнадцати отвечают 404. Начав с каталога, вы полдня будете смотреть на «не найдено» и искать ошибку в коде, которой там нет.
Срезами, а не слоями
Самая дорогая ошибка конкурсного дня — делать по слоям: сначала все DTO, потом все запросы, потом все экраны, потом связать. Звучит аккуратно и почти всегда заканчивается тем, что к вечеру не работает ничего.
Правильно — вертикальными срезами: один запрос доводится до экрана и работает целиком, потом следующий. После каждого среза приложение можно показать. Если день кончится раньше плана — а он кончится, — у вас будет работающая часть, а не набор незаконченных слоёв.
Практическое следствие: не пишите все четырнадцать методов интерфейса сразу. Опечатка в аннотации одного метода роняет создание всего клиента — Retrofit проверяет интерфейс целиком в момент create(). Четырнадцать методов — четырнадцать мест, где искать; один метод — одно.
Спринт как рабочий день
По правилам чемпионата день — это спринт, и у него есть формальная часть, за которую тоже дают баллы:
- утром ветка
sprint-Xотmain; - вечером pull request в
main— и он должен быть закрыт, а не просто создан; - проект лежит файлами, не архивом;
- коммиты по ходу дня, а не один в конце.
«Pull request создан» и «pull request закрыт» — разные вещи, и второе проверяется отдельно. Запрос, оставленный открытым, засчитывается не полностью. Это чистая формальность, которая не требует ни строчки кода, и терять на ней баллы особенно обидно.
Про коммиты стоит сказать отдельно. Один коммит «готово» в конце дня хуже десяти по ходу — и не только по критерию. Когда в шесть вечера что-то ломается, история из десяти коммитов позволяет откатиться на последнее работающее состояние. История из одного — нет.
Приёмочный прогон
Проверять руками по экранам долго и ненадёжно: забывается именно тот случай, который сломан. Быстрее написать короткую программу, которая дёргает все запросы подряд и печатает отчёт.
Вот настоящий вывод такого прогона против стенда:
ПРИЁМОЧНЫЙ ПРОГОН: 14 операций спецификации
1. ✓ Регистрация создан 5o0rfb82cab1lxx
2. ✓ Авторизация токен 224 символов
3. ✓ Информация о пользователе профиль 5o0rfb82cab1lxx
4. ✓ Изменение профиля имя = Пробный
5. ✓ Акции и новости 3 шт., у первой файл banner_y2zhtudxtd.png
6. ✓ Список продуктов 12 товаров, страниц 3
7. ✓ Поиск (тот же путь, filter) 2 найдено
8. ✓ Описание продукта Nike Air Max 270
9. ✓ Создание корзины строка l79q3gp9zhnf01u, count 1
10. ✓ Изменение корзины count = 3
11. ✓ Создание заказа заказ bsvg1v9qupuletd
12. ✓ Создание проекта файл check_e79930sqp5.png
13. ✓ Список проектов 4 шт.
14. ✓ Получение id токена сессий 1, первая ggzj4zb99hl1z2m
15. ✓ Удаление записи сессии 204; токен после этого ПО-ПРЕЖНЕМУ РАБОТАЕТ
Пройдено: 15 из 15
Устроен он просто: одна функция-обёртка, которая ловит исключение и печатает строку.
private var no = 0
private var passed = 0
private suspend fun check(name: String, block: suspend () -> String) {
no++
val r = runCatching { block() }
if (r.isSuccess) {
passed++
println(" %2d. ✓ %-34s %s".format(no, name, r.getOrNull()))
} else {
println(" %2d. ✗ %-34s %s".format(no, name, r.exceptionOrNull()))
}
}
Каждая проверка возвращает строку с тем, что важно увидеть: не «ок», а настоящее значение из ответа. count = 3 в одиннадцатой строке — это доказательство, что изменение количества не просто вернуло 200, а действительно изменило.
Прогон окупается со второго запуска. После любой правки он за несколько секунд отвечает на вопрос «я ничего не сломал?» — а руками этот вопрос стоит десяти минут кликанья по экранам. И он же служит списком задач: каждый ✗ — незакрытый запрос.
Двадцать вещей, на которых теряют баллы
Всё собрано из предыдущих глав — здесь коротким списком, чтобы пробежать глазами перед сдачей.
Сеть и разбор
- Забыли
<uses-permission android:name="android.permission.INTERNET" />. - Базовый адрес без завершающего слэша.
- Тип ответа списка описан как
List<…>вместоPageResponse<…>. - Нет
ignoreUnknownKeys— разбор падает на служебных полях PocketBase. - Одиночную запись пытаются разобрать как конверт.
Авторизация
- Поле входа названо
emailвместоidentity. - Токен хранится в переменной и не переживает перезапуск.
- Заголовок вешается вручную и в каком-то методе забыт.
- Написана ветка
if (code == 401), которой не бывает: здесь 400 и 404.
Формы
- Под полем показан верхний
messageвместо разбора по полю изdata. passwordConfirmпроверили только на клиенте и не отправили.
Каталог и корзина
- Конец страниц определяют по пустому списку, а не по
page >= totalPages. - Пустой результат поиска показан как ошибка, а не как «ничего не найдено».
- Дубли в корзине: повторное нажатие создаёт вторую строку.
- Количество уходит в ноль и минус.
- Товары для корзины запрашиваются по одному — задача N+1.
- Сумма считается по строке
approximateCostвместо числовогоprice.
Файлы и отказы
- Имя файла подставлено как адрес картинки.
- В самолётном режиме приложение падает или показывает пустой экран.
- При выходе не очищены локальные данные — следующий видит чужую корзину.
За полчаса до сдачи
Короткий список того, что делается в самом конце и занимает минуты:
- прогнать приёмочную проверку — все ли запросы живы;
- пройти по всем экранам в самолётном режиме;
- выйти из аккаунта и войти другим пользователем — не видно ли чужого;
- перезапустить приложение — остался ли вход, на месте ли корзина;
- проверить, что проект лежит файлами и собирается из чистой копии;
- закрыть pull request.
Сборка из чистой копии — отдельная ловушка. Проект, который собирается у вас, может не собраться у эксперта, если что-то важное не попало в репозиторий: например, файл с адресом сервера, исключённый в .gitignore вместе с остальными настройками. Проверяется это одной командой — клонировать свой же репозиторий в другую папку и собрать.
Куда углубиться
- Шаблон проекта, заготовленный заранее: зависимости, клиент,
DataStore, базовые состояния экрана. На чемпионате разрешено, и это экономит первый час. MockWebServer— прогон запросов без интернета, полезно, когда сервер площадки лежит.- Генерация клиента из спецификации: почему стоит писать руками, а не доверять генератору — в этой спецификации, например, есть места с нарушенными отступами.
- Замер времени: сколько на самом деле уходит на каждый шаг, если засечь.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- PocketBase — Web API — полное описание запросов к записям.
- Правила доступа и фильтры PocketBase — почему запреты выглядят как 404.
- Retrofit — аннотации запросов — справочник по всем аннотациям сразу.
- Kotlin — runCatching и Result — как ловить исключение значением, а не try-блоком.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.