26.09.2026 Технический разбор RU

Волгатех.Коллектив: личный кабинет преподавателя, когда сервер менять нельзя

Нативное мобильное приложение для личного кабинета работника ПГТУ: расписание, которое открывается без сети, служебная записка по иностранным группам в один шаг и сессия, которая не обрывается от плохой связи. Всё это — на стороне клиента, потому что сервер остаётся прежним.

Никита Конкин

Проект Репозиторий Скачать APK
Языковые версии: RU EN

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

Первая версия вышла 21 сентября, к 24-му их было восемь. Этот разбор — о решениях, которые за ними стоят.

Экран расписания занятий на 11 сентября с четырьмя парами; последняя помечена как занятие двух групп вместе
Расписание дня: цвет недели, тип занятия значком, совместное занятие двух групп — одной карточкой.
Экран обзора недели с карточками дней, числом пар и длительностью окон
Обзор недели: число пар, границы учебного дня и суммарные «окна».
Экран «Иностранные группы» за сентябрь 2026 года: список занятий с флажками и кнопка «Сформировать .docx»
Служебная записка: занятия с иностранными группами за месяц, отмеченные к включению.
Боковое меню приложения: профиль, расписания, иностранные группы, обращения, почта, портал, настройки и выход
Меню: почта и портал открываются внутри приложения.

Зачем переписывать

Цели были сформулированы с самого начала: исправить ошибки, работать быстро, в том числе на старых телефонах, и собираться и под 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.

Выводы

  1. Когда сервер менять нельзя, клиент становится местом исправления данных. Тем важнее, чтобы каждое исправление было точным: совместное занятие сливается, а накладка — нет.
  2. Расписание живёт по часам на стене, а не по моменту времени. Часовой пояс, корректный для моментов, здесь ошибочен.
  3. Сохранённая копия ценна, только если показывается первой. Кэш, который читают после неудачного запроса, не ускоряет ни один запуск.
  4. Хуже выхода из сессии только сессия, которая сломалась молча. Временный сбой и отказ сервера — разные события, и обрабатывать их нужно по-разному.
  5. Выпуск — такая же часть продукта, как код. Сборка с чужим сертификатом не установится, сколько бы в ней ни было исправлений.

Доступность

Приложение распространяется в виде APK для Android; для большинства современных телефонов подходит app-arm64-v8a-release.apk. Исходный код открыт.

Теги

программирование flutter dart мобильная-разработка продукт

Другие обновления по проекту

12.10.2026 Технический разбор Волгатех.Коллектив — мобильный личный кабинет работника ПГТУ

Волгатех.Коллектив: Liquid Glass — стекло, иней и узоры времён года

Как в приложении появилась тема «Liquid Glass»: полупрозрачные панели над цветным фоном, матовое стекло, иней из фрактальных кристаллов и узор по времени года — листья, иней, цв...

12.10.2026 Технический разбор Волгатех.Коллектив — мобильный личный кабинет работника ПГТУ

Волгатех.Коллектив: обновление из приложения, версия для iPhone и компьютеров, напоминания и виджет

Версии 0.5.1 и 0.5.2: приложение само предлагает обновление и проверяет файл перед установкой, работает в браузере — на iPhone без App Store и на компьютере в своей раскладке, н...