Волгатех.Коллектив: личный кабинет преподавателя, когда сервер менять нельзя
Нативное мобильное приложение для личного кабинета работника ПГТУ: расписание, которое открывается без сети, служебная записка по иностранным группам в один шаг и сессия, которая не обрывается от плохой связи. Всё это — на стороне клиента, потому что сервер остаётся прежним.
Волгатех.Коллектив — мобильное приложение для личного кабинета работника ПГТУ: расписание занятий и экзаменов, профиль, почта, портал и одна бюрократическая обязанность, о которой ниже. Это личный проект, а не официальное приложение университета: оно работает с тем же API, что и существующий кабинет, и ничего на сервере не меняет.
Первая версия вышла 21 сентября, к 24-му их было восемь. Этот разбор — о решениях, которые за ними стоят.




Зачем переписывать
Цели были сформулированы с самого начала: исправить ошибки, работать быстро, в том числе на старых телефонах, и собираться и под Android, и под iOS. Исходный кабинет построен на WebView, и на слабых устройствах это заметно по отзывчивости интерфейса. Новое приложение написано на Flutter и рисует интерфейс нативно; сборка под 64-битный телефон занимает около 20 МБ.
Главное ограничение определило всё остальное: приложение — только клиент. Сервер остаётся прежним, и всё, что в его ответах неудобно или неточно, приходится исправлять на стороне телефона — аккуратно и так, чтобы исправление не создавало новых ошибок.
Одна пара — одна карточка
Первое, что обнаружилось в реальных данных: API возвращает отдельное событие для каждой группы. Если две группы занимаются вместе, в одной аудитории и в одно время, расписание показывало две одинаковые карточки.
Решение — сгруппировать события по ключу, описывающему одно физическое занятие:
String _slotKey(ScheduleEvent e) =>
'${e.timeBegin}|${e.timeEnd}|${e.building}|${e.room}|${e.description}|${e.typeWorkName}';
В ключ входят не только время и место, но и дисциплина с типом занятия. Это существенно: если в одно время в одной аудитории стоят две разные дисциплины, это не совместное занятие, а накладка в расписании, и сливать такие события нельзя — преподаватель должен её увидеть. Совместное занятие показывается одной карточкой с пометкой «N групп вместе», а обзор недели считает пары по физическим занятиям, а не по событиям.
Понедельник, который становился воскресеньем
Время в ответах API приходит с часовым поясом: 2026-09-21T09:00:00+03:00. Стандартный разбор переводит такой момент в пояс телефона — и на устройстве, настроенном не на московское время, понедельничные пары оказывались под воскресеньем.
Но расписание — это не момент времени, а время по часам на стене университета. Поэтому смещение пояса отбрасывается, а дата и время берутся буквально:
/// Parse an API timestamp (e.g. "2026-09-21T09:00:00+03:00") as WALL-CLOCK time,
/// ignoring the trailing timezone offset.
DateTime apiWallClock(String s) { ... }
Пара в 09:00 остаётся парой в 09:00 на любом телефоне — ровно так, как она написана в расписании.
Расписание без ожидания сети
В первых версиях сохранённая копия расписания читалась только после неудачного запроса, поэтому каждый запуск ждал сеть. Теперь порядок обратный: сохранённая неделя показывается сразу, а свежая загружается в фоне и заменяет её целиком — чтобы отменённое занятие не задержалось из старой копии. После успешной загрузки в фоне подгружается и следующая неделя.
Попутно выяснилось, что недели нужно отслеживать независимо. Раньше медленный ответ для одной недели мог перезаписать состояние другой: неверный цвет недели, индикатор загрузки, пропадающий раньше времени. Теперь у каждой недели своё состояние — загружена, загружается или ошибка, — и возврат к уже загруженной неделе не порождает нового запроса. Раз копия уже на экране, таймаут соединения сокращён с 40 до 10 секунд.
Сессия, которая не обрывается от плохой связи
Самый неприятный дефект был молчаливым. Если обновление токена срывалось по временной причине — нет сети, таймаут, ошибка сервера, — сессия стиралась, но экран об этом не знал. Приложение выглядело вошедшим, а все запросы проваливались.
Теперь сессия заканчивается только тогда, когда сервер действительно отклоняет токен обновления, и в этом случае приложение возвращает на экран входа с объяснением. Временный сбой сессию не трогает. Запросы, упавшие одновременно, — например, профиль и расписание при запуске, — разделяют одно обновление токена, а не запускают каждый своё.
Служебная записка за один шаг
Раз в месяц преподаватель составляет служебную записку о занятиях с группами иностранных студентов. Все нужные сведения уже есть в расписании, и приложение собирает записку само.
Иностранную группу выдаёт трёхзначный номер, например ИСТ-110:
final _foreignGroup = RegExp(r'^[А-ЯЁA-Z][А-ЯЁа-яёA-Za-z]*-\d{3}$');
Каждое такое занятие становится строкой таблицы по два часа; занятие нескольких групп сразу — одной строкой; последняя ячейка несёт итог месяца, например «2/78». Занятия можно снять, добавить вручную, дисциплины — переименовать покороче; шапка и подпись заполняются один раз и запоминаются. В первые десять дней месяца по умолчанию выбран предыдущий — именно за него в это время и пишут записку.
Результат — файл .docx в шаблоне самого портала, которым можно сразу поделиться. Шаблон получен из заполненной записки служебным скриптом: поля размечены, персональные данные вычищены, а заголовок таблицы повторяется на каждой странице, и строки не разрываются переносом.
Пароль, которого нет
Пароль приложение не хранит нигде. После входа в защищённом хранилище ОС остаются только токены доступа и обновления. Почта и портал открываются внутри приложения, но вход в них выполняется на самих сайтах, и приложение хранит лишь их сессионную cookie, а не учётные данные. По желанию приложение закрывается отпечатком пальца или PIN-кодом.
Одна мелочь из этого же раздела хорошо показывает, как устроен клиент. Люди нередко вводят в поле логина полный корпоративный адрес, а сервер ждёт только имя учётной записи. Приложение отбрасывает домен, обрезает пробелы и приводит логин к нижнему регистру — на своей стороне, не требуя ничего от сервера.
Обновление, которое телефон отказывался ставить
Последний дефект касался не кода, а выпуска. Релизные сборки подписывались отладочным ключом, а его каждая машина CI генерирует заново. В итоге у каждого релиза был свой сертификат, и телефон отказывался ставить обновление поверх: «Приложение не установлено: конфликт пакетов».
Теперь релизы подписываются одним постоянным ключом из секретов репозитория. Сценарий выпуска отказывается публиковать сборку без них и перед публикацией сверяет отпечаток SHA-256 сертификата каждого APK с эталонным. Тем, кто ставил версии до 0.4.1 включительно, старую версию придётся один раз удалить; дальше обновления ставятся поверх.
Дальше
Сборка под iOS проверяется в CI при каждом изменении основной ветки, но подписанный выпуск требует учётной записи разработчика Apple. Самая крупная из оставшихся функций — «Обращения»: пункт меню уже есть, а сам раздел с созданием, комментариями и вложениями ещё впереди. Новости и успеваемость ждут, пока будет понятно, в каком виде их отдаёт API.
Выводы
- Когда сервер менять нельзя, клиент становится местом исправления данных. Тем важнее, чтобы каждое исправление было точным: совместное занятие сливается, а накладка — нет.
- Расписание живёт по часам на стене, а не по моменту времени. Часовой пояс, корректный для моментов, здесь ошибочен.
- Сохранённая копия ценна, только если показывается первой. Кэш, который читают после неудачного запроса, не ускоряет ни один запуск.
- Хуже выхода из сессии только сессия, которая сломалась молча. Временный сбой и отказ сервера — разные события, и обрабатывать их нужно по-разному.
- Выпуск — такая же часть продукта, как код. Сборка с чужим сертификатом не установится, сколько бы в ней ни было исправлений.
Доступность
Приложение распространяется в виде APK для Android; для большинства современных телефонов подходит app-arm64-v8a-release.apk. Исходный код открыт.
- Последняя версия: github.com/nikita-konkin/volgatech.corp/releases
- Исходный код: github.com/nikita-konkin/volgatech.corp
Теги
Другие обновления по проекту
Волгатех.Коллектив: Liquid Glass — стекло, иней и узоры времён года
Как в приложении появилась тема «Liquid Glass»: полупрозрачные панели над цветным фоном, матовое стекло, иней из фрактальных кристаллов и узор по времени года — листья, иней, цв...
Волгатех.Коллектив: обновление из приложения, версия для iPhone и компьютеров, напоминания и виджет
Версии 0.5.1 и 0.5.2: приложение само предлагает обновление и проверяет файл перед установкой, работает в браузере — на iPhone без App Store и на компьютере в своей раскладке, н...