Зашёл я (@Lavs) в «Спортмастер», чтобы купить себе футбольный мяч. В этом деле мне помогла сотрудница Ксения: задала пару вопросов, отвлеклась на смартфон и через минуту протянула подходящий мячик.

Я знал, что на экране у Ксении был не мессенджер с подружками, а внутренние приложения магазина. Дело в том, что в «Спортмастер» я пришёл не только за мячиком. Хабр отправил меня с редакционным заданием: разобраться, как устроена технологическая начинка магазина.

Ребята из SM Lab (IT-компания «Спортмастера») сейчас ищут Android- и iOS-разработчиков. Под катом речь пойдёт о том, чем занимаются команды мобильной разработки: а они ни много ни мало обеспечивают работу всех 533 розничных «Спортмастеров».

А вот и Ксения
А вот и Ксения

Общение с клиентом: продавцы, киоски, кнопки

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

Пока мы с Ксенией говорили о рабочей жизни продавцов, в зале меня нашёл разработчик — Кирилл Леонов, главный программист SM Lab. С ним мы условились встретиться заранее: он должен был провести меня по всем закоулкам магазина и подробно рассказать о технологических решениях.

Заказать спортинвентарь можно самостоятельно, с помощью большого экрана в зале: продавцы называют его киоском
Заказать спортинвентарь можно самостоятельно, с помощью большого экрана в зале: продавцы называют его киоском

Раньше на экранах в магазине открывался обычный сайт, но теперь это Android-приложение. У него много интеграций с бэкендом, и поначалу был общий модуль с приложением продавца, но потом их модули разошлись и киоск стал самостоятельным приложением. Он запускается в монопольном режиме, становясь оболочкой системы. С этим возникали проблемы при обновлении: монопольно можно запустить только одно приложение.

Для решения таких проблем, а также для управления доступом и настройками в киоске используется опенсорсная MDM-система (MDM — Mobile Device Management). Киоск подключается шнуром через Ethernet, так что настроить доступы в корпоративную сеть в нём несложно.

С личными устройствами ситуация в плане безопасности сложнее. Для консультаций сотрудники используют личные Android- (та же MDM-система) и iOS-смартфоны (MobileIron). Но более сложные регламентные операции вроде инвентаризации или переоценки проводятся с корпоративных устройств.

Кстати, все приложения работают без интернета. В каждом «Спортмастере» развёрнут свой сервер, с которым по локальному Wi-Fi общаются все мобильные устройства в магазине.

Через интерфейс киоска можно вызвать продавца, а в некоторых магазинах для этого есть отдельные кнопки в раздевалке. Когда покупатель нажимает на неё, по радиоканалу отправляется сигнал на ретранслятор. С ретранслятора сервер магазина собирает данные и отправляет их по REST API в целевую систему, где создаются задания. И потом с этого же сервера через MQTT-брокер уведомление уходит всем продавцам в магазине.

В приложении «Задачник», которое установлено на каждом корпоративном устройстве, появляется задание на коммуникацию. И первый из сотрудников, кто возьмёт это задание, будет знать, куда ему подойти.

Таск-трекер и сервер в одном приложении

Оказалось, что «Задачник» одновременно является сервером для других приложений app-группы. Через него продавцы авторизуются в системе. В базу данных может писать только «Задачник». Без него не запустятся другие приложения, так что он всегда работает фоном.

В первых версиях «Задачник», уходя в фон, периодически запрашивал разрешение на продолжение работы. Но из-за особенностей iOS от этого решения пришлось отказаться: иногда система неожиданно останавливала работу приложения. В итоге нашёлся костыль, но изящный: в бэкграунде задачник работает как плеер, который воспроизводит тишину. Батарейку это, конечно, жрёт, но не так уж быстро.

Кстати, до того, как разрабы прикрутили генерацию тишины, «Задачник» действительно играл Джеймса Брауна на нулевом уровне громкости.

Когда пользователь работает над заданием, время взятия в работу, все промежуточные действия и время завершения задания фиксируются и отправляются в бэк-офис. Если сеть недоступна, «Задачник» кэширует данные и ждёт, пока появится подключение.

Интересно, что в самом «Задачнике» пользователи не работают: он получает новые задания с сервера, присылает уведомления и распределяет информацию по другим приложениям. Все приложения находятся в app-группе, благодаря чему у них есть общий доступ к файловой системе, User Defaults, Keychain, а также IPC (Inter-Process Communication) — это что-то вроде сокетов. Хранилище в app-группе общее, но владеет им «Задачник», так что другие приложения туда не пишут.

SM Lab не сразу пришли к Realm. Изначально была Core Data и синхронизация справочников занимала два-три часа, что неприемлемо с мобильным устройством. С переходом на Realm синхронизация сократилась до 15 минут без какой-либо оптимизации.

«Но с Realm оказалось не всё так просто: и чтение, и запись должны быть в одном потоке, иначе Realm создаёт для потокобезопасной работы слепок и хранит его в кэше. Используя все в отдельном потоке, мы не учли, что каждое приложение — это тоже отдельный поток. И чем дольше приложения работали, тем больше рос объём кэша, что в итоге приводило к сбою. Кэш становился таких размеров, что не помещался в оперативную память и БД невозможно было использовать.

Поэтому мы перенесли всю работу с базой данных в приложение «Задачник», а остальные приложения стали получать данные от него по IPC. База перестала неограниченно расти».

Кирилл Леонов

Главный программист SM Lab

Ещё одна интересная история с синхронизацией справочников была связана с транспортным слоем. Сначала получение справочников было сделано на MQTT (Message Queuing Telemetry Transport), что позволяло нам гарантировано получать все пакеты даже при нестабильном подключении. Но на практике такое решение оказалось неудачным, так как после суток офлайна накапливалось такое количество данных (иногда до 2–4 Гб), что при узком канале их невозможно было скачать.

Это послужило поводом для перехода на всем привычный REST, что в итоге сократило время синхронизации до 5 минут.

MQTT был оставлен в качестве замены APNS, по нему теперь передаются только уведомления о наличии новых данных. Так удалось избежать необходимости стучаться на сервер каждые N минут, что экономит батарею.

Также при старте проверяем валидность справочников, и если они устарели, то запрещаем работу, пока не будет проведена синхронизация. Ведь для offline-входа пользователю нужен актуальный справочник «Пользователи», а также актуальные цены и товары.

Новые задачи отправляются одновременно всем продавцам в магазине, но ситуаций, когда на вызов прибежали два продавца, не случается. Задания обособленные, устройства друг с другом не взаимодействуют — и выполнить задачу можно только с того устройства, на котором она была начата. Поэтому не возникает ситуаций, когда с двух разных устройств попытались сделать одно и то же.

Почему несколько приложений, а не одно? Потому что в разных магазинах нужны разные наборы функций — пришлось бы для каждого магазина делать свои сборки. А так в каждом магазине устанавливаются только те приложения, которые ему нужны. Благодаря тому, что все приложения находятся в монорепе в GitLab — не надо переключаться между разными репозиториями, что ускоряет разработку.

Чудо-чехол

Смартфон, в котором Ксения искала мяч, на поверку оказался вовсе не смартфоном. Она как раз проводила инвентаризацию с корпоративного iPod Touch. Но сразу я его не признал из-за толстого чехла, в который явно была встроена батарея и ещё какие-то устройства.

Чехол — это батарея на 1 300 mAh, в которую встроен фонарик, сканер штрих-кодов, динамик-пищалка и дополнительный модуль Bluetooth, позволяющий работать с периферией (например с мобильным ридером этикеток). Чехол работает как отдельное устройство, подключённое к iPod через порт Lightning.

Работу всех модулей чехла поддерживает фреймворк, который в SM Lab написали сами.

«Сканер штрих-кодов тут очень шустрый, но на некоторых блестящих и выпуклых поверхностях (например на пупырчатом баскетбольном мяче) он плохо работает, так как лазер отражается в стороны. С таких поверхностей продавцы сканируют штрих-коды с помощью камеры iPod. Также в чехол встроен лазерный указатель со светодиодной подсветкой, который помогает подсветить штрих-код для считывания. И есть возможность ввести штрих-код вручную, если он повреждён и никак не читается».

Кирилл Леонов

Главный программист SM Lab

Встроенный динамик-пищалка помогает продавцам понять, что штрих-код считался. При инвентаризации нет времени смотреть на экран и нужно быстро проверить все штрих-коды. Неверные штрих-коды звучат иначе, чем верные, и совсем не так, как излишек. Благодаря такому подходу появляется возможность держать подсветку экрана устройства на минимальных значениях, что экономит заряд батареи.

Слева тот самый iPod Touch в чехле. Чехол довольно пухлый, но под завязку набит полезными устройствами
Слева тот самый iPod Touch в чехле. Чехол довольно пухлый, но под завязку набит полезными устройствами

Чехол использует собственный Bluetooth-модуль для связи с принтерами этикеток. Apple для работы Bluetooth-устройств требует обязательную сертификацию, что ограничивает модельный ряд оборудования. Для обхода этих ограничений используется Bluetooth чехла.

Для того, чтобы приложения могли работать с принтером, его нужно зарегистрировать в «Задачнике», отсканировав штрих-код на корпусе. Принтеры печатают в формате ZPL (Zebra Printing Language) — это язык разметки для печати. Чтобы было проще верстать этикетки, у SM Lab есть фреймворк — благодаря ему не нужно разбираться в тонкостях языка ZPL.

Также на сервере в магазине можно создать себе штрих-код работника и с помощью сканера на чехле авторизоваться в соответствующем устройстве. Такие штрих-коды даются на определённый срок и действуют в рамках одного магазина.

И само собой встроенная в чехол батарея увеличивает время работы iPod Touch. С чехлом его хватает на день активной работы.

Есть, конечно, с чехлом и сложности: он периодически уходит в сон. И когда он уснул, программно к нему невозможно подключиться — приходится нажимать на физическую кнопку. Из-за этого приходится предупреждать пользователей, что надо нажимать на кнопку.

Проводим инвентаризацию и переоценку

Ранее я упомянул про инвентаризацию — для неё есть отдельное приложение «Выборочная инвентаризация». Часто оно используется для подсчёта товара на одной полке/стеллаже, но иногда инвентаризация охватывает весь товар в магазине.

Возможны ситуации, когда нужно посчитать до 50 тысяч позиций
Возможны ситуации, когда нужно посчитать до 50 тысяч позиций

Также есть отдельное приложение для актуализации цен магазина, называется «Переоценка». Они во многом похожи: есть работа со сканером штрих-кодов и их ввод вручную, есть просмотр товарных карточек и поиск товара в базе по наименованию или другим параметрам.

Но задачи у этих приложений разные. Если «Выборочная инвентаризация» нужна для пересчёта товара, то для «Переоценки» уже нужна печать этикеток. Кроме того «Переоценка» работает с контрольно-идентификационными знаками (КиЗами), электронными ценниками, и через него можно перепечатывать повреждённые штрих-коды.

Когда продавец принимает задачу на инвентаризацию или переоценку, он должен находиться в зале, в зоне приёма внутреннего роутера — чтобы никто не взял эту задачу параллельно. Потом можно спокойно идти на склад, где нет Wi-Fi, и сканировать там товары. А потом, когда сотрудник вернётся в зону с Wi-Fi, всё автоматически отправится на сервер.

При приёме задачи приложение актуализирует справочники, но к устаревшим данным инвентаризация нечувствительна. Например, был завоз, и ещё не внесли информацию по новому товару — тогда в бэк-офис отправляется штрих-код и все проверки проходят уже там.

Ещё через «Переоценку» продавцы обновляют информацию на электронных ценниках. Это устройства с экранами на электронных чернилах: они подключаются к хабу, которым управляет сервер, а приложение интегрировано с API сервера. Такая схема работы позволяет отправлять на сервер одинаковые команды, без учёта тонкостей работы конкретного ценника. Модели бывают разные: какие-то на радиомодуле, другие на Wi-Fi или шине.

Также через приложение для переоценки продавцы работают с КиЗами. Это относительное новшество, в РФ применяется с 2019 года — двухмерный штрих-код, содержащий информацию о товаре и код проверки. В штрих-коде зашифрован производитель, название бренда и продавца. И что самое главное, его невозможно подделать: на каждый товар (например на каждую пару обуви) печатается и наклеивается уникальный КиЗ.

«В целом печать — это потенциально проблемный процесс, поэтому во время внедрения разработчики устраивали Bluetooth-соединению краш-тесты. Выяснилось, что когда соединение с принтером отваливается в момент печати, принтер может зависнуть. Оживить его можно только после выключения питания и переподключения к Bluetooth. Поэтому мы сделали принудительное отключение принтера при сворачивании приложения и принудительное подключение — при возвращении в него».

Кирилл Леонов

Главный программист SM Lab

Человеческий фактор никто не отменял: опытным путём выяснилось, что металлические полки могут экранировать Bluetooth-сигнал, из-за чего приходится по сто раз переподключать принтер. Поэтому сотрудники не кладут принтеры на полки, а закрепляют их на себе специальной клипсой.

Принтер крепится клипсой к специальному ремню — получается довольно удобная конструкция
Принтер крепится клипсой к специальному ремню — получается довольно удобная конструкция

Собираем онлайн-заказы

Для сборки интернет-заказов есть отдельное приложение: логистика происходит на сервере, а к сотруднику приходит уже точное расположение каждого товара (в торговом зале или на складе).

Отгружаем товар

Приложением «Отгрузка» сотрудники магазина пользуются раз в полгода, когда идёт плановая смена одних сезонных товаров на другие. Зеркальное приложение «Приёмка товара» тоже есть.

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

Сгруппированные джемперы
Сгруппированные джемперы

В приложении идёт контроль коробов, пломб на машинах, упаковки. Если сотрудник отсканировал товар и пытается положить его в неправильный короб — будет противный писк.

«Отгрузка» сложнее, чем все другие приложения, так как это не одна задача, а целый набор: сборка, упаковка, отправка машины, коробочный пересчёт (например, можно набрать 100 коробов, но в машину все не поместятся и нужно зафиксировать, какие поместились).

Над этим приложением много работали дизайнеры, и долго прорабатывался UX, чтобы проложить линейный путь для выполнения отгрузки. Всё, что требуется от сотрудника, — это пройтись по чек-листу: машина та самая, водитель тот самый, машина не битая, не вскрытая и так далее — это всё сотрудник отмечает. Если что-то не в порядке — то есть фотофиксация.

Вот как выглядит интерфейс для фотофиксации проблем при отгрузке
Вот как выглядит интерфейс для фотофиксации проблем при отгрузке

Дальше грузятся коробки, которые сотрудник фиксирует, закрывается машина, вешается пломба, которая также фиксируется в приложении. Вся информация в системе и отправляется в бэк-офис. И не надо бегать с бумажками и всё сверять.

Меняем полиграфию в зале

Недавно SM Lab запустили первое приложение, которое не связано с конкретными заданиями — им пользуются в свободном режиме. Оно называется «Оформитель» и нужно для замены полиграфии в зале.

Сотрудник подходит к стенду, сканирует товары — и получает PDF с подробными характеристиками товара для распечатки.

Магазин отправляет данные на сервер, а там они вносятся в шаблоны и отправляются обратно — уже готовые для печати. Если вдруг текст не помещается и едет вёрстка — сотрудник может вручную подправить. А когда конкретный товар не подходит для стенда, приложение не даёт добавить к печати.

Приложение написано с использованием UDF-архитектуры (Unidirectional Data Flow). Некоторое время ушло на то, чтобы сформировать общее видение для всей команды и зафиксировать правила разработки. Но когда всё решили — пошло как по маслу.

«У нас две основные архитектуры: MVP (в основном использовали на старте) и UDF (начали использовать, когда приложения стали сложными). Постепенно на UDF перетаскиваем старые приложения, поэтому в нашем зоопарке есть приложения переходного типа: UDF уже есть, но всё лежит на презентере и на нём все события крутятся. Суть UDF в том, что благодаря разделению приложения на слои легко отделить бизнес-логику от UI и железа (оборудования), а потом переиспользовать её в других приложениях».

Кирилл Леонов

Главный программист SM Lab

Получается, что во многих приложениях SM Lab интерфейс — это чисто view, который на входе получает модель и рендерит себя. UI можно отдельно разрабатывать и легко тестировать. Всё приложение представляет из себя state, и можно легко проверить, правильно ли оно работает без привязки к UI. Поэтому всё UI-тестирование и интеграционные тесты можно заменить unit-тестами.

Ещё я расспросил Кирилла о том, как организован процесс разработки. Ответы на основные вопросы:

Про безопасность

Как у вас обстоят дела с безопасностью?

Кирилл: Пользователи не устанавливают приложения сами. У нас это контролируется и на устройствах стоят сертификаты, ограничивающие действия пользователей, и вредоносные действия они не могут совершить. На корпоративные iPod Touch удалённо накатывается весь нужный магазину пакет.

С личными устройствами всё не так строго, но большинство важных для магазина приложений туда установить нельзя. Сотрудники самостоятельно устанавливают мобильное рабочее место продавца — это приложение помогает консультировать клиентов. Но для его работы необходим корпоративный VPN и учётная запись.

Какие сторонние библиотеки используете?

Кирилл: По возможности мы стараемся не использовать сторонних библиотек. Из стороннего у нас только Moya и Alamofire (работа с сетью, достались по наследству), Realm (база данных), Kingfisher (для подгрузки картинок). И так как мы работаем с железом, у нас есть проприетарная, в виде бинарника библиотека. Потому у нас болит, когда кто-нибудь затаскивает новый фреймворк.

Также одна из особенностей наших приложений — использование MQTT (Message Queuing Telemetry Transport) для передачи мелких пакетов. Эта технология пришла с заводов, где есть робототехника. Основное назначение — работа с телеметрией от датчиков и устройств. У нас на основе этой технологии построено замещение пушей от сервера. Для MQTT мы взяли сторонний фреймворк, форкнули его и развиваем уже для себя.

С Alamofire была проблема, когда мы обновились на новый Xcode. Alamofire тоже собирался обновляться, но мы не могли ждать: приложение не запускалось. Тогда пришлось откатиться назад, поэтому хотим в итоге от него отказаться. Аналогичная проблема была с Realm, там мы также зафиксировали версию, но немного другим способом: скачали исходники и собрали бинарь, который теперь и используем.

А у вас есть свой фреймворк?

Кирилл: У нас много фреймворков: driven.framework решает бизнес-задачи, а остальные разбираются с конкретными целями: генерируют этикетки, например. В driven.framework включено всё, что нужно для поддержки семейства приложений: функции для работы с базой данных и железом, для работы в app-группе, для работы с конфигурацией.

Для вас же подрядчики делают мобильное рабочее место продавца. Как для них поставляете свой фреймворк?

Кирилл: Для себя под капотом используем CocoaPods и локально подключаем все фреймворки. И в целях нераскрытия кода для всех подрядчиков мы вынуждены поставлять им бинарники наших фреймворков. Поэтому специально для них делаем бинарные сборки и передаём их через артефактори (система хранения бинарных ресурсов), туда смотрит CocoaPods и позволяет подгружать бинарные ресурсы. И подрядчики работают в обычном режиме через CocoaPods, указывают адрес артефактори и вытягивают оттуда бинарники, им недоступна кодовая база. Подрядчики получают документацию публичных методов + в Xcode работает QuickHelp, подсвечивая методы и их параметры.

Про дизайн-систему

Как у вас организован UI?

Кирилл: Первые приложения были без дизайна на стандартных эпловских компонентах. Чтобы сделать это красиво, мы пригласили дизайнера, сработались с ним, и в результате появилась дизайн-система. В итоге сейчас всё рисуется в Figma, есть набор экранов, набор компонентов, полностью расписано что и в каких случаях появляется. И дизайн-система постоянно развивается.

Вы используете storyboard или генерите UI-кодом?

Кирилл: У нас есть специально разработанный фреймворк, похожий на SwiftUI (его мы себе позволить не можем, потому что многие iPod в магазинах не поддерживают SwiftUI). Использование фреймворка ускоряет создание экранов. При рендеринге мы передаём ему props — это состояние UI, модель, которая изначально заточена под UDF.

Потому мы переводим старые приложения на этот фреймворк: достаточно иметь модель, закинуть её в рендер и всё отрисуется как нужно. Также в планах подключить автоматическую генерацию шрифтов, цветов, стилей.

Нет ли сложностей с маленькими экранами?

Кирилл: Мы изначально понимали, что будем работать с iPod и маленькими экранами. Поэтому проектировали изначально под них. Сейчас только один размер экрана используется. Но был шаг в другую сторону: хотели запуститься на iPad и для этого сделали интерфейс резиновым.

Про DevOps

Расскажите, как у вас устроен DevOps?

Кирилл: У нас почти всё автоматизировано. Есть стайл-гайды по коду и тестам, отслеживаем покрытие тестами. Также у нас автогенерится документация и схема зависимостей приложений. Есть CI/CD, который занимается нашими сборками и поставками.

Как только создаётся pull-request, CI/CD запускается на сборку и прогоняет тесты по всем приложениям. И как только получишь галочку от CI/CD — то нужно пройти обязательное ревью кода. Мы предпочитаем созваниваться (так есть гарантия, что pull-request не будет долго висеть). CI/CD на GitLab реализован через fastlane, и стараемся унифицировать lane-файл под все команды разработки. Каждые две недели выделяем из команды девопсов: они занимаются ревью, проверкой pull-request, поддержанием CI/CD, собеседованиями, фиксом багов. А остальная часть команды пилит бизнес-фичи. Разработчики развиваются в T-shape специалистов, периодически переходя на другие направления: то работа с драйверами, то интерфейсы, то тесты и так далее.

Заключение

С моим трёхлетним опытом в Android/iOS я ещё не видел настолько проработанных экосистем. Самому захотелось применить некоторые из услышанных практик у себя. Как минимум изучить архитектуру UDF, разработать дизайн-систему с компонентами, убрать Rx из приложений, поработать с IPC и MQTT.

Даже жаль, что в ближайшее время я не планирую менять работу. Если для вас это актуально, то знайте: SM Lab нанимают Android- и iOS-разработчиков.

Зашёл я (@Lavs) в «Спортмастер», чтобы купить себе футбольный мяч. В этом деле мне помогла сотрудница Ксения: задала пару вопросов, отвлеклась на смартфон и через минуту протянула подходящий мячик.

Я знал, что на экране у Ксении был не мессенджер с подружками, а внутренние приложения магазина. Дело в том, что в «Спортмастер» я пришёл не только за мячиком. Хабр отправил меня с редакционным заданием: разобраться, как устроена технологическая начинка магазина.

Ребята из SM Lab (IT-компания «Спортмастера») сейчас ищут Android- и iOS-разработчиков. Под катом речь пойдёт о том, чем занимаются команды мобильной разработки: а они ни много ни мало обеспечивают работу всех 533 розничных «Спортмастеров».

А вот и Ксения
А вот и Ксения

Общение с клиентом: продавцы, киоски, кнопки

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

Пока мы с Ксенией говорили о рабочей жизни продавцов, в зале меня нашёл разработчик — Кирилл Леонов, главный программист SM Lab. С ним мы условились встретиться заранее: он должен был провести меня по всем закоулкам магазина и подробно рассказать о технологических решениях.

Заказать спортинвентарь можно самостоятельно, с помощью большого экрана в зале: продавцы называют его киоском
Заказать спортинвентарь можно самостоятельно, с помощью большого экрана в зале: продавцы называют его киоском

Раньше на экранах в магазине открывался обычный сайт, но теперь это Android-приложение. У него много интеграций с бэкендом, и поначалу был общий модуль с приложением продавца, но потом их модули разошлись и киоск стал самостоятельным приложением. Он запускается в монопольном режиме, становясь оболочкой системы. С этим возникали проблемы при обновлении: монопольно можно запустить только одно приложение.

Для решения таких проблем, а также для управления доступом и настройками в киоске используется опенсорсная MDM-система (MDM — Mobile Device Management). Киоск подключается шнуром через Ethernet, так что настроить доступы в корпоративную сеть в нём несложно.

С личными устройствами ситуация в плане безопасности сложнее. Для консультаций сотрудники используют личные Android- (та же MDM-система) и iOS-смартфоны (MobileIron). Но более сложные регламентные операции вроде инвентаризации или переоценки проводятся с корпоративных устройств.

Кстати, все приложения работают без интернета. В каждом «Спортмастере» развёрнут свой сервер, с которым по локальному Wi-Fi общаются все мобильные устройства в магазине.

Через интерфейс киоска можно вызвать продавца, а в некоторых магазинах для этого есть отдельные кнопки в раздевалке. Когда покупатель нажимает на неё, по радиоканалу отправляется сигнал на ретранслятор. С ретранслятора сервер магазина собирает данные и отправляет их по REST API в целевую систему, где создаются задания. И потом с этого же сервера через MQTT-брокер уведомление уходит всем продавцам в магазине.

В приложении «Задачник», которое установлено на каждом корпоративном устройстве, появляется задание на коммуникацию. И первый из сотрудников, кто возьмёт это задание, будет знать, куда ему подойти.

Таск-трекер и сервер в одном приложении

Оказалось, что «Задачник» одновременно является сервером для других приложений app-группы. Через него продавцы авторизуются в системе. В базу данных может писать только «Задачник». Без него не запустятся другие приложения, так что он всегда работает фоном.

В первых версиях «Задачник», уходя в фон, периодически запрашивал разрешение на продолжение работы. Но из-за особенностей iOS от этого решения пришлось отказаться: иногда система неожиданно останавливала работу приложения. В итоге нашёлся костыль, но изящный: в бэкграунде задачник работает как плеер, который воспроизводит тишину. Батарейку это, конечно, жрёт, но не так уж быстро.

Кстати, до того, как разрабы прикрутили генерацию тишины, «Задачник» действительно играл Джеймса Брауна на нулевом уровне громкости.

Когда пользователь работает над заданием, время взятия в работу, все промежуточные действия и время завершения задания фиксируются и отправляются в бэк-офис. Если сеть недоступна, «Задачник» кэширует данные и ждёт, пока появится подключение.

Интересно, что в самом «Задачнике» пользователи не работают: он получает новые задания с сервера, присылает уведомления и распределяет информацию по другим приложениям. Все приложения находятся в app-группе, благодаря чему у них есть общий доступ к файловой системе, User Defaults, Keychain, а также IPC (Inter-Process Communication) — это что-то вроде сокетов. Хранилище в app-группе общее, но владеет им «Задачник», так что другие приложения туда не пишут.

SM Lab не сразу пришли к Realm. Изначально была Core Data и синхронизация справочников занимала два-три часа, что неприемлемо с мобильным устройством. С переходом на Realm синхронизация сократилась до 15 минут без какой-либо оптимизации.

«Но с Realm оказалось не всё так просто: и чтение, и запись должны быть в одном потоке, иначе Realm создаёт для потокобезопасной работы слепок и хранит его в кэше. Используя все в отдельном потоке, мы не учли, что каждое приложение — это тоже отдельный поток. И чем дольше приложения работали, тем больше рос объём кэша, что в итоге приводило к сбою. Кэш становился таких размеров, что не помещался в оперативную память и БД невозможно было использовать.

Поэтому мы перенесли всю работу с базой данных в приложение «Задачник», а остальные приложения стали получать данные от него по IPC. База перестала неограниченно расти».

Кирилл Леонов

Главный программист SM Lab

Ещё одна интересная история с синхронизацией справочников была связана с транспортным слоем. Сначала получение справочников было сделано на MQTT (Message Queuing Telemetry Transport), что позволяло нам гарантировано получать все пакеты даже при нестабильном подключении. Но на практике такое решение оказалось неудачным, так как после суток офлайна накапливалось такое количество данных (иногда до 2–4 Гб), что при узком канале их невозможно было скачать.

Это послужило поводом для перехода на всем привычный REST, что в итоге сократило время синхронизации до 5 минут.

MQTT был оставлен в качестве замены APNS, по нему теперь передаются только уведомления о наличии новых данных. Так удалось избежать необходимости стучаться на сервер каждые N минут, что экономит батарею.

Также при старте проверяем валидность справочников, и если они устарели, то запрещаем работу, пока не будет проведена синхронизация. Ведь для offline-входа пользователю нужен актуальный справочник «Пользователи», а также актуальные цены и товары.

Новые задачи отправляются одновременно всем продавцам в магазине, но ситуаций, когда на вызов прибежали два продавца, не случается. Задания обособленные, устройства друг с другом не взаимодействуют — и выполнить задачу можно только с того устройства, на котором она была начата. Поэтому не возникает ситуаций, когда с двух разных устройств попытались сделать одно и то же.

Почему несколько приложений, а не одно? Потому что в разных магазинах нужны разные наборы функций — пришлось бы для каждого магазина делать свои сборки. А так в каждом магазине устанавливаются только те приложения, которые ему нужны. Благодаря тому, что все приложения находятся в монорепе в GitLab — не надо переключаться между разными репозиториями, что ускоряет разработку.

Чудо-чехол

Смартфон, в котором Ксения искала мяч, на поверку оказался вовсе не смартфоном. Она как раз проводила инвентаризацию с корпоративного iPod Touch. Но сразу я его не признал из-за толстого чехла, в который явно была встроена батарея и ещё какие-то устройства.

Чехол — это батарея на 1 300 mAh, в которую встроен фонарик, сканер штрих-кодов, динамик-пищалка и дополнительный модуль Bluetooth, позволяющий работать с периферией (например с мобильным ридером этикеток). Чехол работает как отдельное устройство, подключённое к iPod через порт Lightning.

Работу всех модулей чехла поддерживает фреймворк, который в SM Lab написали сами.

«Сканер штрих-кодов тут очень шустрый, но на некоторых блестящих и выпуклых поверхностях (например на пупырчатом баскетбольном мяче) он плохо работает, так как лазер отражается в стороны. С таких поверхностей продавцы сканируют штрих-коды с помощью камеры iPod. Также в чехол встроен лазерный указатель со светодиодной подсветкой, который помогает подсветить штрих-код для считывания. И есть возможность ввести штрих-код вручную, если он повреждён и никак не читается».

Кирилл Леонов

Главный программист SM Lab

Встроенный динамик-пищалка помогает продавцам понять, что штрих-код считался. При инвентаризации нет времени смотреть на экран и нужно быстро проверить все штрих-коды. Неверные штрих-коды звучат иначе, чем верные, и совсем не так, как излишек. Благодаря такому подходу появляется возможность держать подсветку экрана устройства на минимальных значениях, что экономит заряд батареи.

Слева тот самый iPod Touch в чехле. Чехол довольно пухлый, но под завязку набит полезными устройствами
Слева тот самый iPod Touch в чехле. Чехол довольно пухлый, но под завязку набит полезными устройствами

Чехол использует собственный Bluetooth-модуль для связи с принтерами этикеток. Apple для работы Bluetooth-устройств требует обязательную сертификацию, что ограничивает модельный ряд оборудования. Для обхода этих ограничений используется Bluetooth чехла.

Для того, чтобы приложения могли работать с принтером, его нужно зарегистрировать в «Задачнике», отсканировав штрих-код на корпусе. Принтеры печатают в формате ZPL (Zebra Printing Language) — это язык разметки для печати. Чтобы было проще верстать этикетки, у SM Lab есть фреймворк — благодаря ему не нужно разбираться в тонкостях языка ZPL.

Также на сервере в магазине можно создать себе штрих-код работника и с помощью сканера на чехле авторизоваться в соответствующем устройстве. Такие штрих-коды даются на определённый срок и действуют в рамках одного магазина.

И само собой встроенная в чехол батарея увеличивает время работы iPod Touch. С чехлом его хватает на день активной работы.

Есть, конечно, с чехлом и сложности: он периодически уходит в сон. И когда он уснул, программно к нему невозможно подключиться — приходится нажимать на физическую кнопку. Из-за этого приходится предупреждать пользователей, что надо нажимать на кнопку.

Проводим инвентаризацию и переоценку

Ранее я упомянул про инвентаризацию — для неё есть отдельное приложение «Выборочная инвентаризация». Часто оно используется для подсчёта товара на одной полке/стеллаже, но иногда инвентаризация охватывает весь товар в магазине.

Возможны ситуации, когда нужно посчитать до 50 тысяч позиций
Возможны ситуации, когда нужно посчитать до 50 тысяч позиций

Также есть отдельное приложение для актуализации цен магазина, называется «Переоценка». Они во многом похожи: есть работа со сканером штрих-кодов и их ввод вручную, есть просмотр товарных карточек и поиск товара в базе по наименованию или другим параметрам.

Но задачи у этих приложений разные. Если «Выборочная инвентаризация» нужна для пересчёта товара, то для «Переоценки» уже нужна печать этикеток. Кроме того «Переоценка» работает с контрольно-идентификационными знаками (КиЗами), электронными ценниками, и через него можно перепечатывать повреждённые штрих-коды.

Когда продавец принимает задачу на инвентаризацию или переоценку, он должен находиться в зале, в зоне приёма внутреннего роутера — чтобы никто не взял эту задачу параллельно. Потом можно спокойно идти на склад, где нет Wi-Fi, и сканировать там товары. А потом, когда сотрудник вернётся в зону с Wi-Fi, всё автоматически отправится на сервер.

При приёме задачи приложение актуализирует справочники, но к устаревшим данным инвентаризация нечувствительна. Например, был завоз, и ещё не внесли информацию по новому товару — тогда в бэк-офис отправляется штрих-код и все проверки проходят уже там.

Ещё через «Переоценку» продавцы обновляют информацию на электронных ценниках. Это устройства с экранами на электронных чернилах: они подключаются к хабу, которым управляет сервер, а приложение интегрировано с API сервера. Такая схема работы позволяет отправлять на сервер одинаковые команды, без учёта тонкостей работы конкретного ценника. Модели бывают разные: какие-то на радиомодуле, другие на Wi-Fi или шине.

Также через приложение для переоценки продавцы работают с КиЗами. Это относительное новшество, в РФ применяется с 2019 года — двухмерный штрих-код, содержащий информацию о товаре и код проверки. В штрих-коде зашифрован производитель, название бренда и продавца. И что самое главное, его невозможно подделать: на каждый товар (например на каждую пару обуви) печатается и наклеивается уникальный КиЗ.

«В целом печать — это потенциально проблемный процесс, поэтому во время внедрения разработчики устраивали Bluetooth-соединению краш-тесты. Выяснилось, что когда соединение с принтером отваливается в момент печати, принтер может зависнуть. Оживить его можно только после выключения питания и переподключения к Bluetooth. Поэтому мы сделали принудительное отключение принтера при сворачивании приложения и принудительное подключение — при возвращении в него».

Кирилл Леонов

Главный программист SM Lab

Человеческий фактор никто не отменял: опытным путём выяснилось, что металлические полки могут экранировать Bluetooth-сигнал, из-за чего приходится по сто раз переподключать принтер. Поэтому сотрудники не кладут принтеры на полки, а закрепляют их на себе специальной клипсой.

Принтер крепится клипсой к специальному ремню — получается довольно удобная конструкция
Принтер крепится клипсой к специальному ремню — получается довольно удобная конструкция

Собираем онлайн-заказы

Для сборки интернет-заказов есть отдельное приложение: логистика происходит на сервере, а к сотруднику приходит уже точное расположение каждого товара (в торговом зале или на складе).

Отгружаем товар

Приложением «Отгрузка» сотрудники магазина пользуются раз в полгода, когда идёт плановая смена одних сезонных товаров на другие. Зеркальное приложение «Приёмка товара» тоже есть.

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

Сгруппированные джемперы
Сгруппированные джемперы

В приложении идёт контроль коробов, пломб на машинах, упаковки. Если сотрудник отсканировал товар и пытается положить его в неправильный короб — будет противный писк.

«Отгрузка» сложнее, чем все другие приложения, так как это не одна задача, а целый набор: сборка, упаковка, отправка машины, коробочный пересчёт (например, можно набрать 100 коробов, но в машину все не поместятся и нужно зафиксировать, какие поместились).

Над этим приложением много работали дизайнеры, и долго прорабатывался UX, чтобы проложить линейный путь для выполнения отгрузки. Всё, что требуется от сотрудника, — это пройтись по чек-листу: машина та самая, водитель тот самый, машина не битая, не вскрытая и так далее — это всё сотрудник отмечает. Если что-то не в порядке — то есть фотофиксация.

Вот как выглядит интерфейс для фотофиксации проблем при отгрузке
Вот как выглядит интерфейс для фотофиксации проблем при отгрузке

Дальше грузятся коробки, которые сотрудник фиксирует, закрывается машина, вешается пломба, которая также фиксируется в приложении. Вся информация в системе и отправляется в бэк-офис. И не надо бегать с бумажками и всё сверять.

Меняем полиграфию в зале

Недавно SM Lab запустили первое приложение, которое не связано с конкретными заданиями — им пользуются в свободном режиме. Оно называется «Оформитель» и нужно для замены полиграфии в зале.

Сотрудник подходит к стенду, сканирует товары — и получает PDF с подробными характеристиками товара для распечатки.

Магазин отправляет данные на сервер, а там они вносятся в шаблоны и отправляются обратно — уже готовые для печати. Если вдруг текст не помещается и едет вёрстка — сотрудник может вручную подправить. А когда конкретный товар не подходит для стенда, приложение не даёт добавить к печати.

Приложение написано с использованием UDF-архитектуры (Unidirectional Data Flow). Некоторое время ушло на то, чтобы сформировать общее видение для всей команды и зафиксировать правила разработки. Но когда всё решили — пошло как по маслу.

«У нас две основные архитектуры: MVP (в основном использовали на старте) и UDF (начали использовать, когда приложения стали сложными). Постепенно на UDF перетаскиваем старые приложения, поэтому в нашем зоопарке есть приложения переходного типа: UDF уже есть, но всё лежит на презентере и на нём все события крутятся. Суть UDF в том, что благодаря разделению приложения на слои легко отделить бизнес-логику от UI и железа (оборудования), а потом переиспользовать её в других приложениях».

Кирилл Леонов

Главный программист SM Lab

Получается, что во многих приложениях SM Lab интерфейс — это чисто view, который на входе получает модель и рендерит себя. UI можно отдельно разрабатывать и легко тестировать. Всё приложение представляет из себя state, и можно легко проверить, правильно ли оно работает без привязки к UI. Поэтому всё UI-тестирование и интеграционные тесты можно заменить unit-тестами.

Ещё я расспросил Кирилла о том, как организован процесс разработки. Ответы на основные вопросы:

Про безопасность

Как у вас обстоят дела с безопасностью?

Кирилл: Пользователи не устанавливают приложения сами. У нас это контролируется и на устройствах стоят сертификаты, ограничивающие действия пользователей, и вредоносные действия они не могут совершить. На корпоративные iPod Touch удалённо накатывается весь нужный магазину пакет.

С личными устройствами всё не так строго, но большинство важных для магазина приложений туда установить нельзя. Сотрудники самостоятельно устанавливают мобильное рабочее место продавца — это приложение помогает консультировать клиентов. Но для его работы необходим корпоративный VPN и учётная запись.

Какие сторонние библиотеки используете?

Кирилл: По возможности мы стараемся не использовать сторонних библиотек. Из стороннего у нас только Moya и Alamofire (работа с сетью, достались по наследству), Realm (база данных), Kingfisher (для подгрузки картинок). И так как мы работаем с железом, у нас есть проприетарная, в виде бинарника библиотека. Потому у нас болит, когда кто-нибудь затаскивает новый фреймворк.

Также одна из особенностей наших приложений — использование MQTT (Message Queuing Telemetry Transport) для передачи мелких пакетов. Эта технология пришла с заводов, где есть робототехника. Основное назначение — работа с телеметрией от датчиков и устройств. У нас на основе этой технологии построено замещение пушей от сервера. Для MQTT мы взяли сторонний фреймворк, форкнули его и развиваем уже для себя.

С Alamofire была проблема, когда мы обновились на новый Xcode. Alamofire тоже собирался обновляться, но мы не могли ждать: приложение не запускалось. Тогда пришлось откатиться назад, поэтому хотим в итоге от него отказаться. Аналогичная проблема была с Realm, там мы также зафиксировали версию, но немного другим способом: скачали исходники и собрали бинарь, который теперь и используем.

А у вас есть свой фреймворк?

Кирилл: У нас много фреймворков: driven.framework решает бизнес-задачи, а остальные разбираются с конкретными целями: генерируют этикетки, например. В driven.framework включено всё, что нужно для поддержки семейства приложений: функции для работы с базой данных и железом, для работы в app-группе, для работы с конфигурацией.

Для вас же подрядчики делают мобильное рабочее место продавца. Как для них поставляете свой фреймворк?

Кирилл: Для себя под капотом используем CocoaPods и локально подключаем все фреймворки. И в целях нераскрытия кода для всех подрядчиков мы вынуждены поставлять им бинарники наших фреймворков. Поэтому специально для них делаем бинарные сборки и передаём их через артефактори (система хранения бинарных ресурсов), туда смотрит CocoaPods и позволяет подгружать бинарные ресурсы. И подрядчики работают в обычном режиме через CocoaPods, указывают адрес артефактори и вытягивают оттуда бинарники, им недоступна кодовая база. Подрядчики получают документацию публичных методов + в Xcode работает QuickHelp, подсвечивая методы и их параметры.

Про дизайн-систему

Как у вас организован UI?

Кирилл: Первые приложения были без дизайна на стандартных эпловских компонентах. Чтобы сделать это красиво, мы пригласили дизайнера, сработались с ним, и в результате появилась дизайн-система. В итоге сейчас всё рисуется в Figma, есть набор экранов, набор компонентов, полностью расписано что и в каких случаях появляется. И дизайн-система постоянно развивается.

Вы используете storyboard или генерите UI-кодом?

Кирилл: У нас есть специально разработанный фреймворк, похожий на SwiftUI (его мы себе позволить не можем, потому что многие iPod в магазинах не поддерживают SwiftUI). Использование фреймворка ускоряет создание экранов. При рендеринге мы передаём ему props — это состояние UI, модель, которая изначально заточена под UDF.

Потому мы переводим старые приложения на этот фреймворк: достаточно иметь модель, закинуть её в рендер и всё отрисуется как нужно. Также в планах подключить автоматическую генерацию шрифтов, цветов, стилей.

Нет ли сложностей с маленькими экранами?

Кирилл: Мы изначально понимали, что будем работать с iPod и маленькими экранами. Поэтому проектировали изначально под них. Сейчас только один размер экрана используется. Но был шаг в другую сторону: хотели запуститься на iPad и для этого сделали интерфейс резиновым.

Про DevOps

Расскажите, как у вас устроен DevOps?

Кирилл: У нас почти всё автоматизировано. Есть стайл-гайды по коду и тестам, отслеживаем покрытие тестами. Также у нас автогенерится документация и схема зависимостей приложений. Есть CI/CD, который занимается нашими сборками и поставками.

Как только создаётся pull-request, CI/CD запускается на сборку и прогоняет тесты по всем приложениям. И как только получишь галочку от CI/CD — то нужно пройти обязательное ревью кода. Мы предпочитаем созваниваться (так есть гарантия, что pull-request не будет долго висеть). CI/CD на GitLab реализован через fastlane, и стараемся унифицировать lane-файл под все команды разработки. Каждые две недели выделяем из команды девопсов: они занимаются ревью, проверкой pull-request, поддержанием CI/CD, собеседованиями, фиксом багов. А остальная часть команды пилит бизнес-фичи. Разработчики развиваются в T-shape специалистов, периодически переходя на другие направления: то работа с драйверами, то интерфейсы, то тесты и так далее.

Заключение

С моим трёхлетним опытом в Android/iOS я ещё не видел настолько проработанных экосистем. Самому захотелось применить некоторые из услышанных практик у себя. Как минимум изучить архитектуру UDF, разработать дизайн-систему с компонентами, убрать Rx из приложений, поработать с IPC и MQTT.

Даже жаль, что в ближайшее время я не планирую менять работу. Если для вас это актуально, то знайте: SM Lab нанимают Android- и iOS-разработчиков.

Комментарии (5)


  1. Boomburum
    06.10.2022 22:05
    +1

    Немножко не в тему, но недавно наткнулся на вашу викторину (в приложении как раз) — толком ничего не выиграл (кроме баллов и скидок от партнёров), но вот подборка вопросов и механика заданий это прям кайф!


  1. Alexeydax
    06.10.2022 22:43

    Спасибо за статью. Только, "Fastlane" для Gitlab.


    1. RobertLis
      07.10.2022 08:23
      +1

      Если быть до конца точными, то «fastlane»: в документации со строчной.


  1. Archybald
    07.10.2022 06:49
    +2

    Ксения прекрасна!:)


  1. opion
    07.10.2022 07:24
    +2

    Ксения страшно красивая ^_^