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

Илья, iOS‑разработчик в Naumen, рассказывает, как пришел к подходу, при котором разработчику достаточно описать новое свойство, а интерфейс собирается автоматически.
Почему задача оказалась сложнее?
Все началось с настройки сжатия изображений перед отправкой на сервер. На первый взгляд задача выглядела вполне стандартной: подобрать параметры, проверить результат и убедиться, что все работает как нужно.
Но довольно быстро возник другой вопрос: как проверять изменения без постоянной пересборки приложения?
Для разработчика это не так критично, а вот для аналитиков на приемке и тестировщиков каждая новая проверка требовала участия разработчика. Тогда появилась идея вынести параметры в отдельный экран настроек.
Почему обычный экран настроек не решил проблему?
Сначала мы решили добавить переключатели, поля ввода и другие элементы интерфейса. Но появилась новая сложность: поддерживать такой экран вручную неудобно.
Чтобы добавить новую настройку, нужно было каждый раз:
добавлять свойство;
добавлять соответствующий UI‑компонент;
настраивать обработку;
связывать с хранилищем данных.
Я начал искать подход, при котором разработчику не нужно отдельно поддерживать интерфейс настроек. Хотелось, чтобы достаточно было просто описать новую настройку, а все остальное система делала сама.
В этот момент я вспомнил про Reflection. В Swift этот механизм ограничен и фактически работает как интроспекция, но даже этих возможностей оказалось достаточно для решения задачи.
Как сделать так, чтобы экран собирался автоматически?
В основе подхода лежит декларативный принцип: разработчик описывает свойства объекта настроек и добавляет к ним метаданные, например, название настройки или связи с другими параметрами.
Дальше система анализирует структуру объекта, определяет типы данных и автоматически подбирает нужные UI‑компоненты:
для булевых значений — переключатели;
для текста — поля ввода;
для чисел — поля с ограничением на числовой ввод.
Что изменилось после внедрения такого подхода?
Теперь для добавления новой настройки достаточно описать новое свойство и добавить необходимые метаданные. После этого настройка автоматически появляется в интерфейсе.
По моей оценке, трудозатраты на работу с настройками сократились примерно на 80–90%. Кроме того, уменьшилось количество дублирующего кода, а интерфейс стал более единообразным и предсказуемым для пользователей.
→ Подробнее своим опытом Илья поделился в статье.
Как я научил свою читалку различать примечания и комментарии в «Войне и мире»
Я не профессиональный программист. Пишу Android-читалку MRead для себя и для тех, кому тоже надоели комбайны. Код закрытый, всё работает локально, без серверов и трекинга. Раздаю через RuStore, 4PDA и GitHub.
В версии 1.4.0 я наконец победил баг, который меня лично бесил как читателя. Расскажу именно про него, потому что задача оказалась интереснее, чем выглядела.
Проблема
Открываю «Войну и мир». В одном предложении сразу два маркера сноски: примечание (перевод французского) и научный комментарий. В разных изданиях они нумеруются независимо, поэтому в тексте легко встретить два маркера с одинаковой цифрой. Старая логика по тапу вытаскивала из текста ближайшую цифру и искала абзац, начинающийся с этого числа, по всем главам, и показывала первое совпадение. Итог: тап по «6» открывал не ту сноску, а тап по числу вроде «1805» вообще пытался найти несуществующую сноску.
Почему наивный подход не работает
Цифра это не идентификатор. Она повторяется и в каждой главе, и между разделами «Примечания» и «Комментарии». А вот в исходнике всё однозначно: каждый маркер это ссылка с уникальным адресом.
EPUB: 1 FB2: [6] и {4}
То есть книга всегда знает правильный ответ. Беда была в том, что мой парсер при импорте выбрасывал href и оставлял только видимый текст маркера.
Что я поменял
При разборе HTML сохраняю не только текст абзаца, но и ссылки-сноски с точной позицией маркера. Позицию считаю надёжно: оборачиваю маркер невидимыми символами из Private Use Area до очистки текста, а после очистки нахожу их и вычисляю смещение. Так цифры в обычном тексте (годы, числа) не путаются с маркерами.
Позиции храню в абсолютных координатах главы. Это важно, потому что мой пагинатор режет абзацы на куски по строкам. Абсолютные смещения переживают нарезку без отдельной возни в пагинаторе.
По тапу беру не цифру, а ссылку под пальцем, и резолвлю сноску по точному id из нужного файла. Для FB2 пришлось ещё и сохранить id секций сносок при конвертации в HTML, потому что примечания и комментарии лежат в отдельных главах.
Старый поиск по цифре оставил как запасной вариант для книг без нормальной разметки, но убрал срабатывание на голые числа.
Результат: тап по примечанию открывает перевод, тап по комментарию открывает комментарий, а год «1805» больше никого не трогает.
Контекст для тех, кому интересно
Рендер текста у меня не WebView, а нативный движок на Canvas с собственной пагинацией (Jetpack Compose, Kotlin). Это даёт контроль над переносами, выравниванием по ширине и стабильной привязкой цитат и закладок, но за каждую такую фичу приходится платить ручной работой вроде этой истории со сносками.
Что ещё в 1.4.0, кратко
• Полнотекстовый поиск по PDF (для PDF с текстовым слоем)
• Озвучивание (TTS): голоса, скорость, таймер сна, автопереход, пауза с памятью места
• Передача слова во внешний словарь и контекст предложения в экспорт Anki
• Настраиваемые блоки настроек чтения, межабзацный интервал, Bold/Italic
• Полки, фоновый импорт из папок, оценки и заметки, Material You
Ссылки
• RuStore
• GitHub
• 4PDA
Если у вас была книга FB2, переоткройте её, чтобы заработали точные сноски (HTML генерится при импорте). EPUB подхватит сам.
Буду рад замечаниям по подходу. Если делали резолв сносок иначе, расскажите как, мне правда интересно.
Обратная сторона рабочих чатиков
Андрей Врацкий начал строить рынок корпоративных мессенджеров в 2015 году, когда WhatsApp и Telegram только начинали проникать в рабочие чаты, а безопасники в компаниях делали вид, что все ок и деловая переписка — это личная ответственность сотрудника.
На создание продукта ушло четыре года. Когда eXpress вышел в 2019-м, то выяснилось: все уже так привыкли к Telegram и WhatsApp, что менять ничего не хотят.
Прошло семь лет. Сегодня иностранные мессенджеры заблокированы, отечественный* удален из App Store на неопределенный срок. На этом фоне спрос на eXpress вырос в четыре раза — и это пока Telegram все еще работает. Что будет, когда перестанет?
Хотя по мнению Врацкого, хороший продукт будет востребован вне зависимости от обстоятельств и критерий его успеха — это конкурентоспособность на мировом рынке.
Подробнее — во втором выпуске подкаста «IT-фронтир».
Как я ускорил бэкапы в 20 раз и обошёл ловушку Jsoup: развитие самописной Android‑читалки MRead (v1.3.0)
Всем привет! Не так давно я рассказывал, как боль от перегруженных интерфейсов заставила меня открыть Android Studio и написать собственную читалку с кастомным движком рендеринга и точным выделением текста.
Статья получила теплый отклик и в комментариях набежало много отличных предложений. В этом посте я хочу поделиться техническими решениями, которые вошли в крупное обновление 1.3.0.
1. Бэкапы и боль от Storage Access Framework (SAF)
В приложении есть функция бэкапа: упаковка базы данных Room, настроек и распакованных HTML‑глав с картинками в один ZIP‑архив. Изначально я писал файлы напрямую в OutputStream, полученный через ContentResolver (SAF). Итог: библиотека на 500 МБ архивировалась около 5 минут. SAF проводит проверки безопасности для каждого записываемого чанка, что убивает I/O операции.
Решение: сборка архива переехала во внутренний кэш приложения. Туда пишем без ограничений SAF — буфером по 64 КБ и уровнем сжатия BEST_SPEED (картинки уже сжаты, гнать их через BEST_COMPRESSION бессмысленно). Когда ZIP готов целиком, он одним куском копируется в пользовательскую папку через SAF — вместо тысяч мелких защищённых записей получается одна
2. Material You: как получить правильные цвета обоев
При внедрении динамических тем (Android 12+) я столкнулся с тем, что стандартный вызов dynamicLightColorScheme().background на многих устройствах выдает просто унылый белый или бледно‑серый цвет, игнорируя сочные оттенки обоев.
Решение: Самые насыщенные цвета из системной палитры Monet хранятся в secondaryContainer и surface. Решение нашлось в самой палитре Monet: наиболее насыщенные цвета живут в secondaryContainer и surface, а не в background. Переориентировал маппинг цветов приложения на эти слоты и интерфейс действительно ожил. Теперь интерфейс действительно реагирует на смену обоев. Плюс привязал OnSharedPreferenceChangeListener, чтобы тема менялась мгновенно на всех экранах без перезапуска.
3. Странности парсинга FB2 и баги Jsoup
Иногда вместо обложки FB2 парсер ставил черно‑белую картинку из середины книги. FB2 хранит все изображения в тегах <binary> в конце файла в хаотичном порядке. Если тег <coverpage> отсутствует, старый алгоритм просто брал первую попавшуюся картинку из бинарной кучи.
Я переписал фоллбэк: теперь, если явной обложки нет, Jsoup ищет первый тег <image> прямо внутри <body> книги.
Попутно всплыло неочевидное поведение Jsoup: если атрибут отсутствует, attr() возвращает пустую строку, а не null — это задокументировано, но интуитивно ожидаешь null. Из‑за этого Элвис‑операторы (?:) молча проглатывали пустую строку вместо ухода в fallback. Написал строгую обертку takeIf { it.isNotEmpty() }, и теперь обложки извлекаются безошибочно.
4. Изолированный свайп яркости в Compose
Нужно было добавить регулировку яркости свайпом по левому краю экрана. Проблема: в режиме вертикального скролла (VerticalPager) свайпер страниц перехватывает вертикальные жесты на себя.
Решение: перехватывать жест на фазе Initial — до того, как пейджер успевает его обработать. Если касание началось в левых 15% ширины экрана, событие забирается себе и до пейджера не доходит.
Помимо этого в релизе 1.3.0:
Добавлен полноэкранный просмотрщик иллюстраций с pinch‑to‑zoom (на основе detectTransformGestures).
Написан собственный File Picker со сканированием вложенных папок и извлечением книг прямо из ZIP‑архивов на лету.
Добавлен поворот страниц для PDF с сохранением состояния в SharedPreferences.
Разделен UI верхнего меню: закладки теперь можно переименовывать, а тап по номеру страницы открывает быстрый переход.
Добавлен множественный выбор в библиотеке (массовое добавление на полки/удаление/скрытие).
Ссылки:
🤗 Привет всем!
😻 Обновление HalChat for Android: v1.0.2 (Vote or don't vote)
Что нового?
😌Теперь есть описание чатов.
🤖 Добавлено обновление данных чата.
😉 Добавлены метаданные файлов HD - мгновенное определение что за файл или какие размеры изображения.
😇 Доступны опросы/голосования в чатах.
Что исправлено?
🤓 Если получение или расшифровка пароля была прервана, то теперь он запросит заново, и пароль будет доступен в любом случае.
🫢 Теперь при получении пароля от чата, в списке чатов сообщения будут расшифроваться.
😎 Исправлена система файлов в чате, они будут отображаться всегда и в том числе изображения (исправлен баг).
До новых встреч!
Google Play: https://play.google.com/store/apps/details?id=halwarsing.net.halchatandroid
RuStore: https://www.rustore.ru/catalog/app/halwarsing.net.halchatandroid
HalChat Web: https://halch.at/c/tZgWWT
GitHub: https://github.com/halwarsing/HalChat/tree/dev
Собеседование. Часть 3: Магия коллекций в Kotlin — от лямбд до скрытых аллокаций
В прошлых частях мы препарировали алгоритмы и заглянули под капот хэш-таблиц. Сегодня поговорим о том, за что разработчики так искренне любят Kotlin — о коллекциях.
На собеседованиях этот блок вопросов — мой любимый детектор. Он отлично показывает, умеет ли человек отличать красивый синтаксический сахар от суровой реальности JVM.
Уровень 1: Синтаксический сахар и интерфейсы
Я спрашиваю: «В чем фундаментальная разница между List и MutableList в Kotlin?» Ожидаемый ответ: Kotlin изящно разделил интерфейсы на читаемые (read-only) и изменяемые. У List просто нет методов add или remove. Это спасает нас от случайных мутаций состояния, особенно когда мы гоняем данные в реактивном UI.
Далее переходим к функциям-расширениям. Кандидат пишет классическую цепочку: users.filter { it.age > 18 }.map { it.name }
Выглядит чисто. Здесь же я подкидываю вопрос про reduce и fold. Оба метода сворачивают коллекцию, но если кандидат бездумно использует reduce, я с легкой улыбкой спрашиваю: «А что будет, если список окажется пустым?» Будет больно и UnsupportedOperationException. Поэтому fold с его стартовым значением — выбор тех, кто хочет спать спокойно.
Уровень 2: Срываем покровы компилятора
Когда кандидат уверенно жонглирует лямбдами, пора заглянуть под капот. Я задаю вопрос: «А что конкретно создается в памяти, когда ты вызываешь listOf(1, 2, 3) или mapOf(1 to "A")?»
Многие верят в уникальную магию Kotlin. Но продвинутый разработчик знает, что мы всё еще живем в мире JVM.
mapOf()под капотом вернетjava.util.LinkedHashMap. Но! КонтрактMapне гарантирует сохранение порядка. Если вам критически важен порядок вставки, не надейтесь на реализацию «под капотом» — пишитеlinkedMapOf()явно, иначе в один прекрасный день обновление языка сломает вам логику.listOf(1, 2, 3)вернет не обычныйjava.util.ArrayList. Он вернет внутренний классjava.util.Arrays$ArrayList. И разница тут колоссальная. Это fixed-size обертка над массивом. Если любитель хардкора решит сделать(list as MutableList).add(4), он моментально получит краш в рантайме.
Далее вопрос на засыпку: «Если мы в цикле вызываем filter { ... }, не убиваем ли мы Garbage Collector созданием анонимных классов для лямбд?» Отличный кандидат вспомнит про модификатор inline. Компилятор физически вставляет тело функции в место вызова, поэтому аллокации объектов лямбд не происходит.
Уровень 3: Экспертный взгляд и ловушка Sequences
Вроде бы inline нас спас? И да, и нет. Тут начинается территория архитектуры и производительности. Я возвращаю кандидата к его коду: users.filter { it.age > 18 }.map { it.name }
«Модификатор inline спас нас от лямбд, — говорю я. — Но что будет, если в списке 100 000 юзеров? Что произойдет с памятью?»
Эксперт должен увидеть угрозу: каждая стандартная функция (filter, map) создает новую промежуточную коллекцию. Сначала создастся список из 50 000 взрослых юзеров, а потом еще один из 50 000 их имен. Это колоссальная аллокация памяти.
«Как этого избежать?» Ответ: использовать Sequences — users.asSequence().filter {...}.map {...}.toList(). Последовательности работают лениво, протаскивая каждый элемент по цепочке поштучно, без создания огромных временных списков.
Финальная ловушка. Я спрашиваю: «Значит ли это, что нужно использовать asSequence() вообще везде?» Истинный эксперт скажет: Нет. У Sequence есть свои скрытые налоги: создание объектов-оберток (TransformingSequence, FilteringSequence) и накладные расходы на виртуальные вызовы next()/hasNext() для каждого элемента. На коротких списках обычная цепочка filter/map отработает быстрее. Бенчмарки показывают, что Sequence начинает выигрывать только на объемах примерно от 1 000 элементов (в зависимости от тяжести цепочки). Не стоит заниматься преждевременной оптимизацией там, где она не нужна.
Резюме
Коллекции в Kotlin невероятно удобны. Но за синтаксический сахар всегда нужно платить. Важно видеть, как человек переходит от слепого восторга красивым кодом к прагматичному пониманию
Помогите, кто чем может. Яндекс пробил дно
У меня айфон с маленьким экраном. Предпочитаю компактные модели, чтобы умещались в любом кармане. Это доставляет и некоторые неудобства. Например, часть приложений плохо работают, элементы интерфейса перекрывают друг друга, и из-за этого некоторые функции становятся недоступны. Иногда с такой проблемой я сталкивался в приложении Яндекс.Такси.
В связи с тем, что с продукцией Apple в России в последнее время ситуация постоянно ухудшается, планирую перейти на Android. Нашел подходящую модель на Яндекс.Маркете смартфон Conquest F3 Plus. Одна проблема — в этой модели экран еще меньше, чем у меня сейчас. Значит, есть риск, что приложения, которые глючили на старом смартфоне, вообще не смогут работать на новом.
С данным вопросом я обратился в поддержку Яндекса. Я был уверен, что получу точный ответ, будет работать приложение Яндекс.Go на интересующем меня устройстве или не будет. Ведь что может быть проще? Любой разработчик может, даже если не знает точно, в эмуляторе задать указанное разрешение экрана и прогнать тесты.
Ответом поддержки я был, мягко говоря, ошарашен. Ниже привожу текст нашего диалога со скринами.
Здравствуйте! Будет ли работать ваше приложение на вот такой модели смартфона?
https://market.yandex.ru/cc/9aPY2a
Разрешение экрана 540x1200.
Данную информацию вы можете уточнить в магазине приложений, из которого вы хотите скачать наше приложение
Я хочу купить новый смартфон, но мне нужно знать, будут ли работать нужные приложения. Ваше приложение очень плохо работает на экране с разрешением 1344x750. Хоть и работает. Какое разрешение поддерживает yandex.go? Будет ли оно работать на экране 540x200?
Данную информацию уточнить не получится, подсказать смогут в магазине приложений
Что значит, не получится? Напишите разработчикам, и они вам скажут.
Пожалуйста, обратитесь в магазин приложений для уточнения минимальных системных требований для корректной работы приложения
Какой магазин?
Магазин приложений вашего устройства

Итак, поддержка Яндекс.Go не смогла ответить на вопрос о системных требованиях собственного приложения. Неожиданно. У меня нет ни малейших идей, как такое стало возможно.
В связи с этим я решил обратиться за помощью к товарищам по отрасли. Может, кто-нибудь из читателей Хабра пробовал ставить Яндекс.Go на Android 12 с разрешением экрана 540x1200. Нормально работает?
Восстановление аккаунта в эфемерном мессенджере, не ломая приватность? Легко!
RCQ это анонимный мессенджер: без телефона и почты, история только на устройстве, сервер хранит минимум. Цена такой модели: потерял телефон, и аккаунт (сам UIN, твоя личность в сети) потерян навсегда. Это отпугивало тех, кому нужен постоянный аккаунт. Мы добавили фразу восстановления и постарались не превратить приватный мессенджер в обычный облачный.
Сразу: Android-клиент теперь в проде, фраза в нём есть. Ссылка в конце.
Аккаунт это ключи
Логина и пароля нет. Аккаунт это пара ключей: X25519 (шифрование) и Ed25519 (подпись). UIN это просто ручка на сервере, привязанная к публичному ключу. Приватные ключи не покидают устройство. Отсюда: бэкап аккаунта это бэкап ключа, а восстановление это доказать серверу, что ключ у тебя.
Откуда фраза
При создании аккаунта генерируется случайный seed на 32 байта. Из него детерминированно выводятся оба ключа через HKDF-SHA256 с фиксированными info-строками:identity_priv = HKDF-SHA256(seed, "rcq-recovery-x25519-v1", 32) signing_priv = HKDF-SHA256(seed, "rcq-recovery-ed25519-v1", 32)
Из одного seed всегда те же ключи, значит достаточно сохранить seed. Кодируем его в 24 слова по BIP39 (как в криптокошельках): 256 бит энтропии плюс 8 бит контрольной суммы, режется по 11 бит, словарь на 2048 слов. Контрольная сумма ловит опечатки.
Восстановление
На новом устройстве вводишь 24 слова:
Из слов получается seed, из seed те же ключи.
Клиент берёт у сервера одноразовый челлендж.
Подписывает его Ed25519-ключом, шлёт подпись.
Сервер проверяет подпись против публичного ключа и отдаёт UIN и токен.
Приватный ключ никуда не передаётся, пароль нигде не хранится. Это challenge-response: перехват челленджа бесполезен, он одноразовый.
Что возвращается
Возвращается личность: UIN, ключи, контакты, группы, ожидающие сообщения. НЕ возвращается локальная история переписки, она только на устройстве (зашифрованный бэкап истории это отдельная фича на будущее). Активные ratchet-сессии (forward secrecy) сбрасываются, собеседники видят смену ключа, как в Signal.
Почему это не ломает приватность
Тонкий момент, проговорим его подробно. Восстановимая фраза это постоянный секрет с полным доступом навсегда. Пока seed лежит только в зашифрованном хранилище приложения, удалил приложение не записав фразу, и всё стёрлось, эфемерность сохраняется. Как только выписал 24 слова, появляется вечная точка восстановления, это другая модель угроз.
Баланс такой: seed есть у каждого нового аккаунта, но воспользоваться им (записать фразу) это твой осознанный выбор, не навязанный. Плюс фразу прячем за подтверждением (+PIN), чтобы её не выгребли с разблокированного на минуту телефона.
Кросс-платформенно
Деривация и словарь одинаковы на Android и iOS. Мы прогнали обе реализации (CryptoKit и наш Android-стек) на одном seed и сравнили побайтово: одинаковые ключи, одинаковые слова. Фразу с Android можно ввести на iPhone и наоборот.
Границы
История чатов пока не восстанавливается.
Аккаунты, созданные до фичи, фразы не имеют (их ключи не из seed), для них будет экспорт сырого ключа (но для этого мы и в бете, так что на релизе такого не случится).
Это не мультидевайс: ввести фразу на втором телефоне можно, но это переезд, а не одновременная работа, ratchet-сессии разойдутся.
Код клиента открыт, Android в проде. Будем рады разбору, особенно по криптографии.
Скачать APK (Alpha)/iOS TF (Beta): https://rcq.app/
GH: https://github.com/rcq-messenger
Собеседование. Часть 2: От структур данных до магии Load Factor и data class’ов
В прошлом выпуске мы выяснили, как простая задача на разворот массива вскрывает понимание вычислительной сложности. Сегодня мы поговорим о структурах данных и специфике языка программирования.
Мой второй любимый блок вопросов плавно перетекает от базовых коллекций к особенностям Kotlin и внутреннему устройству хэш-таблиц. Я оцениваю знания градационно: от того, что должен понимать начинающий специалист, до глубокого видения платформы.
Уровень 1: Начинающие специалисты и базовые структуры
Начинаем с разминки. Я прошу объяснить разницу между Array, ArrayList и LinkedList. Это фундамент, без которого сложно двигаться дальше. Если кандидат понимает структурную разницу, я спрашиваю про скорость доступа к произвольному элементу (Time Complexity):
Array (Массив): Непрерывный блок памяти фиксированного размера. Чтение по индексу происходит мгновенно, вычислительная сложность O(1).
ArrayList: Умная обёртка над массивом, умеющая динамически расширяться (путем копирования элементов в новый массив при переполнении). Доступ по индексу также O(1).
LinkedList (Связный список): Элементы разбросаны в памяти, каждый узел знает только о своем соседе. Чтобы найти нужный элемент, нужно последовательно пройти по цепочке. Скорость доступа — O(N).
Если специалист отвечает на это уверенно, значит, базовое понимание Computer Science заложено верно.
Уровень 2: Переход к Kotlin
Дальше я меняю плоскость и перехожу к синтаксису. Вопрос: «В чем разница между обычным class и data class в Kotlin?» Ожидаемый ответ на этом этапе: data class из коробки генерирует полезные методы, избавляя разработчика от написания бойлерплейта. Компилятор самостоятельно создает equals(), hashCode(), toString(), метод copy() и componentN() для деструктуризации.
Затем я прошу уточнить целевое использование. Кандидат должен пояснить, что data class нужен для хранения данных (например, моделей из сети) или состояния UI. Главная особенность в том, что объекты data class’ов сравниваются по содержимому (значениям полей), а не по ссылке в памяти.
Уровень 3: Углубленное понимание платформы А теперь самое интересное — мы сплетаем теорию структур данных и специфику Kotlin воедино.
Я спрашиваю: «Отлично, data class переопределяет метод hashCode(). А для чего именно он нужен? Как он используется под капотом?» Здесь требуется рассказать про принципы работы HashMap или HashSet: Метод hashCode() возвращает число, определяющее, в какую «корзину» (bucket) внутреннего массива попадет объект. Если хэши совпадают (коллизия), применяется метод equals(), чтобы найти точный объект внутри этой корзины.
И: «Что такое Load Factor в хэш-таблице? И что произойдет, если мы установим его слишком высоким (например, 0.95)?»
Правильный ответ: Load Factor (по умолчанию 0.75) — это метрика того, насколько может быть заполнена таблица до автоматического увеличения её размера (rehash). Если установить высокое значение, корзины переполнятся. Возникнет лавина коллизий. В результате хэш-таблица внутри одной корзины деградирует в LinkedList! Скорость доступа падает до линейной O(N) (или O(log N) для деревьев в новых версиях), лишая структуру её главного преимущества.
Резюме: Алгоритмы и структуры данных — это, по сути, сухая теория. Для меня как для интервьюера гораздо важнее то, как человек применяет её на практике.
В мобильной разработке нам гораздо реже приходится реализовывать сложные алгоритмы с нуля, чем ребятам на бэкенде. Но у нас своя специфика — жесткие ограничения по ресурсам устройства. Я не требую энциклопедических знаний. Я задаю простые, последовательные вопросы, чтобы понять: осознает ли человек, что неверно выбранная коллекция может привести к жесточайшим просадкам UI, фризам и неконтролируему расходу памяти.
Именно умение связать теоретическую алгоритмику с физическими ограничениями мобильного устройства показывает мне, насколько специалист действительно готов к реальной коммерческой разработке.
Собеседование. Часть 1: Как простая задача на разворот массива вскрывает понимание Computer Science
За свою карьеру я провел сотни технических собеседований на самые разные грейды — от джунов до системных архитекторов. И делал я это в разных локациях: в России, Европе и США. Процессы найма везде немного отличаются, но есть подходы, которые работают безотказно в любой точке земного шара.
Многие кандидаты боятся алгоритмических секций, ожидая зубодробительных задач с LeetCode. Но моя цель — не завалить, а понять инженерное мышление. Поэтому я часто начинаю с элементарной задачи: дан массив чисел, его нужно отзеркалить (перезаписать в обратном порядке).
Эта задача — идеальная «лесенка», раскрывающая реальный уровень инженера. Давайте пройдем по ней вместе.
Шаг 1: Уровень Джуниора. Просто сделай это
От джуна я жду умения перевести бизнес-требование в код. Самый очевидный способ решить задачу в лоб — создать второй массив и скопировать туда элементы с конца.
fun reverseArrayNaive(arr: IntArray): IntArray {
val result = IntArray(arr.size)
for (i in arr.indices) {
result[i] = arr[arr.size - 1 - i]
}
return result
}
Если код написан без ошибок с индексами — отлично. Если человек путается и не может подойти к задаче — для меня это красный флаг. Если код готов, я задаю первый вопрос: «Какова вычислительная сложность?». Ожидаемый ответ: сложность O(N) по времени, так как мы проходим массив один раз.
Шаг 2: Уровень Мидла. Экономим память
Переходим на следующий уровень. Я спрашиваю: «А что со сложностью по памяти?». Кандидат логично отвечает, что раз мы создаем массив того же размера, сложность — O(N).
Усложняем задачу: «Представь устройство с жестким лимитом ресурсов. Нам нельзя выделять память под второй массив. Как переписать алгоритм, чтобы сложность по памяти стала O(1)?»
Продвинутый разработчик сразу предложит in-place решение: менять элементы местами с начала и с конца.
fun reverseArrayInPlace(arr: IntArray) {
var left = 0
var right = arr.size - 1
while (left < right) {
val temp = arr[left]
arr[left] = arr[right]
arr[right] = temp
left++
right--
}
}
Шаг 3: Уровень Синьора. Психологическая ловушка
Если in-place вариант написан, я подкидываю вопрос с подвохом: «В первом варианте цикл делал N итераций. Во втором указатели встретились посередине, то есть цикл выполнился N/2 раз. Уменьшилась ли вычислительная сложность по времени?»
И тут многие радостно отвечают: «Да! Мы сократили операции в два раза, код стал быстрее!». И это ловушка.
Правильный ответ: Нет, сложность осталась O(N). Давайте посчитаем атомарные операции присваивания:
В наивном подходе мы делали 1 присваивание за итерацию. Цикл шел N раз. Итого: N операций.
В in-place подходе мы делаем swap. Это три операции (
temp = a,a = b,b = temp). Цикл идет N/2 раз. Умножаем 3 на N/2 и получаем 1.5 × N операций!
С точки зрения процессора мы не сэкономили время, а совершили даже больше базовых действий. Мы просто обменяли такты на память. В нотации Big O константы отбрасываются, поэтому оба алгоритма линейные. Но синьор обязан видеть код насквозь, понимая его цену на уровне регистров.
За 10 минут с помощью одной задачи мы проверили:
Умение писать циклы (Джун).
Понимание Big O и расхода памяти (Мидл).
Понимание реальной цены оптимизаций (Синьор).
Это не спортивное программирование с хитрыми математическими трюками. Это проверка базовой инженерной гигиены.
TalkBack в 2ГИС
Уже несколько лет мы поддерживаем VoiceOver на iOS — это был сложный и многоступенчатый путь к доступности приложения. После релиза мы увидели, что это реально работает, и людям с нарушением зрения стало проще пользоваться 2ГИС. В этом году мы решили, что пришло время для Android.
Под капотом — собственный мини-фреймворк
Дальше рассказывает ведущий разработчик Дмитрий Торопчин.
Когда начинаешь делать доступность под Android, кажется, что всё уже предусмотрено. Каждый View знает, как предоставить своё описание для служб доступности, как выполнять действия и какие события отправлять после их выполнения.
2ГИС устроен немного иначе: наш интерфейс написан на Qt Quick и с точки зрения Android это один пустой View. Никаких кнопок, списков и текстовых полей TalkBack «внутри» не видит. Сам Qt предоставляет кроссплатформенный API для работы со службами доступности из Qt Quick, но его платформенная интеграция для Android на данный момент поддерживает весьма ограниченный набор методов, которого категорически не хватает для разработки по-настоящему удобных и доступных приложений.
Поэтому мы решили написать свой мини-фреймворк для работы со службами доступности Android из Qt Quick. Основную архитектуру мы подсмотрели в Qt:
визуальные элементы в Qt Quick размечаются через attached-свойства;
из этой разметки получается дерево accessibility-узлов, управляемое из C++;
это дерево accessibility-узлов предоставляется службам доступности Android через virtual view hierarchy посредством JNI.
При этом мы добавили поддержку многих недостающих API из Android SDK:
научились работать с текстовыми полями ввода;
научились описывать коллекции элементов;
поддержали автоматическую прокрутку списков при перемещении accessibility-фокуса;
поддержали детализацию навигации по тексту: озвучку любого элемента можно прослушать как целиком, так и по словам или по символам.
Пользовательские сценарии
Мы честно признали, что озвучить всё приложение сразу невозможно. Поэтому сделали MVP. Теперь можно:
найти место и узнать актуальную информацию о нём;
прослушать карточку организации и быстро сориентироваться в деталях;
позвонить по контактному номеру прямо из карточки;
построить маршрут и пройти его шаг за шагом;
понять, какой транспорт подходит, где садиться и на какой остановке выходить.
Для этих сценариев мы разметили 555 UI‑элементов.
Мы продолжим собирать обратную связь, будем расширять сценарии и думать, как сделать автоматическую проверку доступности в тестах.
Критический успех
4 июня собираемся с мобильными разработчиками в атмосфере любимых фэнтези и настольных ролевых игр — на Alfa Mobile D&D party. Впереди приключение для истинных авантюристов: объединяйтесь в отряды и открывайте секреты башни.
Друиды поделятся мудростью:
С техническим лидером Android-разработки Виталием Перятиным обсудим, что на самом деле под капотом у MCP. Спойлер: всё просто
С ведущим iOS-разработчиком Петросом Тепояном погрузимся в мир ИИ-разработки и приключений, или Туда и обратно
Завершим игру вечеринкой. Советуем подумать над образом своего персонажа: за 3 лучших подарим подарки!
Где: в секретном баре в Москве. Локацию пришлём с соколом в письме.
Регистрируйтесь на приключение!

Ближайшие события
Как сделать мобильное приложение для УК, которое не удалят?
CTO Doubletapp Никита Анчутин выступил на «Неспальном митапе» от Сделка.рф — неформальной встрече девелоперов и цифровизаторов. Никита рассказал, как сделать приложение для управляющей компании, которым жильцы реально будут пользоваться, а не удалят сразу после установки.
Часто приложения УК не приживаются: жильцы ими не пользуются, а УК не получают обратной связи. Причина — разрыв между ожиданиями пользователей и видением заказчика. В видео — инструкция, как создать цифровой инструмент, который станет качественным посредником между сторонами:
поможет УК слышать жильцов и оперативно решать проблемы;
даст жителям удобный доступ к услугам и каналам коммуникации;
объединит интересы обеих сторон в одном интерфейсе.
В видео — разбор: как совместить потребности жильцов и УК; какие функции критически важны для обеих сторон; каких ошибок заказчикам стоит избегать при постановке задач.
Будет полезно девелоперам, продуктовым менеджерам, представителям УК и застройщиков, UX/UI‑дизайнерам и IT‑руководителям.
Doubletapp — IT‑компания, разрабатывающая мобильные приложения с 2015 года. Мы создаём эффективные инструменты для бизнеса и удобные решения для пользователей.
Инструменты, которые упрощают iOS-разработку
Старый код усложняет рефакторинг, тесты в команде запускаются по‑разному, баги не воспроизводятся на хорошем Wi‑Fi, а после обновления инструментов локальная сборка начинает расходиться с CI — по отдельности все это мелочи, но именно они постепенно начинают тормозить разработку.

Ринат, iOS‑разработчик Naumen, рассказал об инструментах, которые помогают ему решать такие задачи и упрощать повседневную работу.
Periphery: поиск мертвого кода в Swift‑проектах
Со временем в любом проекте появляется код, который уже не используется: старые методы и фичи, забытые extensions и helpers. Все это усложняет навигацию и мешает быстро понять архитектуру проекта.
Periphery помогает находить такие места и наводить порядок перед изменениями в кодовой базе.
Как использую
Запускаю Periphery перед рефакторингом — например, когда нужно обновить модуль профиля с сотнями файлов.
periphery scanПосле сканирования инструмент показывает классы, методы, свойства, enum cases, imports и другие элементы. Так проще понять, что действительно участвует в работе приложения.
Что важно знать: результаты всегда нужно проверять вручную. Инструмент может не учитывать динамические вызовы, reflection, Objective-C runtime, storyboard-ссылки или код, который используется через строки.
Network Link Conditioner: тестирование слабой сети
Во время разработки приложение чаще всего тестируется в почти идеальных условиях. Но у пользователей все может работать иначе.
Network Link Conditioner — инструмент от Apple, который помогает эмулировать разные сетевые условия. Например, индикатор загрузки крутится бесконечно, повторная попытка не срабатывает, время ожидания слишком короткое, а пользователь не получает понятного сообщения об ошибке.
Как использую
Обычно проверяю сценарии авторизации, оплаты, загрузки медиа и офлайн‑режимы. Для этого включаю профиль вроде плохого 3G, высокой задержки или потери пакетов и смотрю, как приложение ведет себя в нестабильной сети.
Что важно знать: проверять стоит не только низкую скорость интернета, но и нестабильность сети. А еще важно не забывать выключать Conditioner после проверки :)
just: короткие команды вместо длинных инструкций
В iOS‑проектах быстро накапливаются команды, которые приходится запускать постоянно: тесты, форматирование, генерация ресурсов. Со временем это превращается либо в огромный онбординг‑документ, либо в постоянный поиск нужной команды в документации.
just собирает основные сценарии работы в одном месте и запускает их через короткие понятные команды. В итоге justfile становится чем‑то вроде живой документации проекта.
Как использую
Чтобы каждый раз не вспоминать синтаксис, храню основные сценарии работы в justfile.
test:
xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15'
format:
swiftformat .
swiftlint
clean:
rm -rf ~/Library/Developer/Xcode/DerivedDataПосле этого вместо длинных команд достаточно написать:
just test
just format
just cleanЧто важно знать: just не заменяет CI, Makefile или build system. Это скорее удобный слой для повседневных команд. Поэтому лучше держать justfile простым и не превращать его в большой набор скриптов.
Mint: фиксация версий CLI-инструментов на Swift
Когда у разработчиков разные версии линтеров, форматтеров и других CLI‑инструментов, могут появиться расхождения. Mint помогает зафиксировать набор инструментов внутри проекта и сделать локальный запуск ближе к CI, чтобы у всей команды был одинаковый результат.
Как использую
Вместо глобальной установки SwiftLint, SwiftFormat, XcodeGen или других CLI‑инструментов можно хранить версии в Mintfile и запускать их одинаково у всех разработчиков.
mint run realm/SwiftLint
mint run nicklockwood/SwiftFormatЧто важно знать: Mint полезен именно для Swift CLI-пакетов. Для Ruby-gems, Node.js-инструментов или системных утилит понадобятся другие менеджеры. Также важно кэшировать установленные бинарные файлы в CI, иначе сборки могут тратить лишнее время на установку инструментов.

Save the date: встречаемся 22 апреля на iOS Meetup Wildberries & Russ
22 апреля в 19:00 мск приглашаем на iOS-митап. В программе три технических доклада и нетворкинг с инженерами, которые ежедневно строят мобильную разработку в Wildberries & Russ. Поговорим про автоматизацию релизного процесса, масштабируемое UI-тестирование и тонкости работы с файловой системой iOS.
Регистрация
Доклады:
— Автоматизация релизов в Wildberries | Севастьян Жуков, Deploy Lab Team Lead
Как команда с нуля создала инструмент для управления релизным процессом мобильного приложения и масштабировала его на другие продукты компании. Разберём этапы автоматизации и работу с App Store API: управление релизами и отслеживание их статуса.
— UI-тестирование приложения Wildberries | Руслан Колчаков, iOS TestLab Lead и Валерий Карачаков, iOS TestLab Dev
Руслан расскажет про вызовы при организации тестирования, инфраструктуру и метрики здоровья TestLab. Валерий дополнит докладом про распределённое UI-тестирование на динамически формируемом кластере раннеров: как избежать простоев, эффективно утилизировать ресурсы и ускорить тестирование.
— Работа с файловой системой на iOS | Александр Игнатьев, iOS-разработчик команды Асто
Разберём нюансы работы с файлами, структуру iOS Sandbox и App Group как способ выйти за её пределы.
⏹️Формат: офлайн в Москве + онлайн-трансляция
Регистрация
От хакатонного эксперимента до релиза, или как появился параллельный режим в CarPlay
Раньше при подключении CarPlay пользователи видели в приложении 2ГИС на телефоне «заглушку» — почти пустой экран. И наш iOS‑разработчик Ваня задумался: «Почему бы не добавить полезности?»
Идея родилась на внутреннем хакатоне. Он решил не переносить интерфейс целиком, а разделить роли между устройствами. Телефон — для действий. CarPlay — для результата.
Собрал прототип, а затем команда подхватила инициативу. И это одна из самых быстрых задач, которая дошла до релиза.

Это интересно, потому что:
новый сценарий для автомобилистов: теперь можно взаимодействовать с приложением на телефоне, пока навигация идёт в CarPlay;
красивый обход архитектурного ограничения;
инженерная инициатива, которая с хакатона дошла до прода.
Как стать IOS-разработчиком?
Недавно мы делали подборку необходимых инструментов для Android-разработчиков, но на этом мир операционных систем не заканчивается — чтобы развиваться, важно выбрать ту, которая больше всего подходит именно вам.
Если нравится экосистема Apple и хочется работать в закрытой, но при этом цельной среде — попробуйте разработку под IOS. На Хабр Карьере как раз есть множество учебных программ по этому направлению, а ниже — подборка ключевых инструментов "яблочного" разработчика.
— SWIFT. Основной язык программирования для iOS.
— Xcode. Официальная среда разработки от Apple.
— CocoaPods. Менеджер зависимостей для подключения библиотек.
— Realm. Локальная база для хранения данных в приложении.
— GCD. Технология для работы с многопоточностью.
— UIKit. Фреймворк для создания интерфейсов приложений.
Как стать Android-разработчиком?
Войти в Android-разработку проще, чем кажется: начать можно с освоения базовых инструментов, а затем постепенно углубляться и закреплять знания на практике. Главное — двигаться последовательно и не пытаться охватить всё сразу.
Сегодня мы собрали подборку для тех, кто хочет стать Android-разработчиком и разобраться в основе профессии. А на Хабр Карьере есть все, чтобы выстроить понятный маршрут: сотни учебных программ — от базовых знаний до практики и первых проектов.
Java. Базовый язык программирования для Android
Kotlin. Кроссплатформенный язык программирования
Android Studio. Среда разработки (IDE) для приложений
Mockito. Фреймворк для модульного тестирования
→ Еще больше полезных инструментов для разработчиков на нашей витрине
Сегодня с 10 часов адрес https://api.telegram.org/bot недоступен. Этот адрес взаимодействия ботов с платформой телеграмм. Как итог телеграмм боты "легли". Если точнее, телеграмм вызывает обработчик, но обратно в телеграмм отправить ничего нельзя.

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