Русский
Русский
English
Статистика
Реклама

Разработка мобильных приложений

Data driven подход для усиления защиты Android

01.03.2021 16:18:41 | Автор: admin


Мы делаем все, чтобы платформа Androidбыла безопасной для всех пользователей на всех устройствах. Каждый месяц выходятобновления системы безопасностис исправлениями уязвимости, найденными участниками программыVulnerability Rewards Program (VRP). Однако мы также стараемся защищать платформу от других потенциальных уязвимостей, напримериспользуя компилятори улучшая тестовую среду. Экосистема Android включает в себя устройства с самыми разными возможностями, поэтому все решения должны быть взвешенными и должны учитывать доступные данные.

В этой статье рассказано о том, как мы выбираем средства безопасности для конкретных обстоятельств и как они реализуются.

Обеспечение безопасности Android подразумевает комплексный подход. Чтобы затруднить потенциальное использование уязвимостей, мы принимаем решения на основе данных, руководствуясь сразу несколькими принципами и методами. Когда речь идет об усилении защиты платформы, необходимо ответить на следующие вопросы:

  • Какие у нас есть данные и как они могут помочь в принятии решений?
  • Какие средства предотвращения атак доступны? Как их можно улучшить? В каких ситуациях их следует применять?
  • Какие проблемы могут возникнуть при использовании тех или иных средств безопасности? Какие возможные расклады следует принять во внимание?

Принципы, на которые мы опираемся при выборе средств безопасности, отражают наш общий подход к защите пользователей платформы Android.

Принятие решений по обеспечению безопасности на основе данных


Чтобы выяснить, для каких компонентов платформы будут эффективны те или иные решения, мы обращаемся к различным источникам. ПрограммаAndroid Vulnerability Rewards Program(VRP) едва ли не самый информативный из них. Наши инженеры по безопасности анализируют все уязвимости, обнаруженные участниками программы, определяя их первопричины и уровень серьезности (на основеэтих рекомендаций). Кроме того, есть внутренние и внешние отчеты об ошибках. Они помогают выявлять уязвимые компоненты, а также фрагменты кода, которые часто вызывают сбои. Зная, как выглядят такие фрагменты, и представляя себе серьезность и частоту ошибок, возникающих из-за них, мы можем принимать взвешенные решения о том, какие средства безопасности будут наиболее эффективными.


Уязвимости с высоким и критическим уровнем серьезности, исправленные в бюллетенях по безопасности Android в 2019 г.

Однако не стоит полагаться только на отчеты об уязвимостях. Они изначально дают искаженную картину, так как специалисты по безопасности часто обращают внимание на горячие зоны, то есть на те области, где ранее уже были найдены уязвимости (например,Stagefright). Или они могут искать уязвимости там, где их проще обнаружить с помощью готовых решений. Например, если на платформе GitHub будет опубликован какой-нибудь аналитический инструмент безопасности, многие специалисты будут им пользоваться.

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

В рамках программы Android VRP мы поощряем разработчиков, которые добавляютполные цепочки уязвимостей,позволяющие проследить весь процесс атаки от начала до конца. Как правило, злоумышленники используют сразу несколько уязвимостей, и в подобных цепочках эти связки хорошо видны, поэтому они очень информативные. Наши инженеры по безопасности анализируют как цепочки целиком, так и их отдельные звенья и пытаются обнаружить в них новые стратегии атак. Такой анализ помогает определить стратегии, помогающие предотвратить последовательное использование уязвимостей (например,случайное распределение адресного пространстваи методыControl Flow Integrity), а также понять, можно ли уменьшить масштабы атаки, если процесса получил нежелательный доступ к ресурсам.

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

Чтобы понимать текущие угрозы безопасности и прогнозировать будущие тенденции, нужно постоянно держать руку на пульсе сообщества специалистов безопасности, в частности:

  • тесно сотрудничать со сторонними специалистами по вопросам безопасности;
  • читать тематические издания и посещать конференции;
  • изучать технологии, используемые вредоносным ПО;
  • отслеживать последние разработки в сфере безопасности;
  • принимать участие в сторонних проектах, таких какKSPP, syzbot, LLVM, Rust ит.д.

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

Почему усиление защиты необходимо


Усиление защиты и предотвращение атак


Анализ данных помогает выявлять области, в которых эффективные средства предотвращения атак могут устранить целые классы уязвимостей. Например, если в некоторых компонентах платформы появляется много уязвимостей из-за ошибок целочисленного переполнения, следует использовать санитайзер неопределенного поведения (UBSan), например Integer Overflow Sanitizer. Если часто наблюдаются уязвимости, связанные с доступом к памяти, необходимо использоватьпрограммы распределения памяти с усиленной защитойAndroid 11 они включены по умолчанию) и средства предотвращения атак (например,Control Flow Integrity), устойчивые к переполнению памяти и уязвимостям Use-After-Free.

Прежде чем поговорить об использовании данных, мы предложим классификацию инструментов для усиления защиты платформы. Вот основные сегменты, на которые можно разбить все эти инструменты (хотя некоторые средства и методы могут относиться сразу к нескольким из них):

  • Средства устранения эксплойтов
    • Средства детерминированного устранения уязвимостей в среде выполнениявыявляют неопределенное или нежелательное поведение и прерывают выполнение программы. Это исключает повреждение данных в памяти, при этом сохраняется вероятность лишь менее серьезных сбоев. Часто такие средства можно применять точечно, и они все равно будут эффективными, так как рассчитаны на отдельные ошибки. Примеры:санитайзер для целочисленного переполненияиBoundsSanitizer.
    • Средства снижения воздействия эксплойтовпредотвращают переход от одной уязвимости к другой или получение возможности выполнения кода. В теории эти средства могут полностью устранять некоторые уязвимости. Однако чаще всего они ограничивают возможности их использования. В результате злоумышленникам приходится тратить больше времени и ресурсов на разработку эксплойта. Часто эти средства задействуют весь объем памяти, занимаемый процессом. Примеры: случайное распределение адресного пространства, Control Flow Integrity (CFI), стековый индикатор, добавление тегов к памяти.
    • Преобразование компилятора, изменяющие неопределенного поведения в определенное на этапе компиляции. В результате злоумышленники не могут воспользоваться неопределенным поведением, напримернеинициализированной областью памяти. Пример: инициализация стека.
  • Декомпозиция архитектуры
    • Отдельные блоки разделяются на мелкие компоненты с меньшими привилегиями. В результате воздействие уязвимостей в этих компонентах уменьшается, так как злоумышленник не получает прежнего доступа к системе. Этот метод удлиняет цепочки уязвимостей, а также усложняет доступ к конфиденциальным данным и дополнительным путям повышения привилегий.
  • Песочницы и изоляция
    • Здесь действует принцип, схожий с декомпозицией. Процессу выделяется минимальный набор разрешений и возможностей, необходимых для нормальной работы (часто с помощью обязательного и/или избирательного контроля доступа). Как и в случае с декомпозицией, песочница ограничивает возможности злоумышленников и делает уязвимости в этих процессах менее значимыми благодаря принципу минимальных привилегий. Примеры:разрешения в Android,разрешения в Unix,возможности Linux,SELinux иSeccomp.
  • Использование языков с безопасной обработкой памяти
    • Языки программирования C и C++, в отличие от Java, Kotlin и Rust, не обеспечивают достаточный уровень безопасности памяти. Учитывая, чтобольшинствоуязвимостей в Androidсвязаны с памятью, мы применяем двусторонний подход: улучшаем безопасность языков C/C++ и одновременно рекомендуем использовать более надежные языки программирования.

Реализация этих инструментов


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


Декомпозиция архитектуры и изоляция медиа фреймворков в историческом контексте

Объекты удаленных атак (NFC, Bluetooth, Wi-Fi и медиаконтент) традиционно сопряжены с самыми серьезными уязвимостями, поэтому усиление их безопасности должно стать приоритетной задачей. Как правило, появление этих уязвимостей вызвано самыми распространенными первопричинами, которые выявляют в рамках программы VRP, и недавно мы добавили для всех них санитайзеры.

Средства предотвращения атак удобно использовать для библиотек и процессов, которые задают границы безопасности или находятся в них (например,libbinder, а также стандартные библиотекиlibui,libcore иlibcutils), так как они не привязаны к конкретным процессам. Однако эти библиотеки отвечают за эффективную и стабильную работу систем, поэтому перед тем как применять тот или иной метод, необходима серьезная гарантия того, что он усилит безопасность.

Наконец, важно обеспечить защиту ядра, учитывая высокий уровень его привилегий. У всех кодовых баз разные характеристики и функциональность, поэтому и вероятность появления уязвимостей в них отличается. Главные критерии здесь стабильность и производительность. Следует применять только эффективные средства безопасности, которые не будут мешать пользователям работать. Поэтому прежде чем выбрать оптимальную стратегию усиления защиты, мы тщательно анализируем все доступные данные, связанные с ядром.
Подход, основанный на данных, дал ощутимые результаты. После обнаружения уязвимости Stagefright в 2015 году мы стали получать сообщения о большом количестве другихкритическихуязвимостей мультимедийной платформы Android. Ситуацию усложняло то, что многие из них были доступны удаленно. Мы провелимасштабную декомпозицию системы Android Nougat иускорили исправление уязвимостей в мультимедийных компонентах. Благодаря этим изменениям в 2020 году не было ни одного сообщения о критических уязвимостях в мультимедийных платформах, к которым можно получить доступ через Интернет.

Как принимается решение о развертывании


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

Производительность


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

При оценке производительности мы обращаем внимание не только на процессорное время, но и на использование памяти, длину кода, время работы от батареи ислучаи зависания интерфейса. Чтобы убедиться в эффективной работе того или иного средства во всей экосистеме Android, особенно важно проверить перечисленные параметры на устройствах начального уровня.

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

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

  • Выборочно отключить средства предотвращения атак для функций, существенно влияющих на производительность. Как правило, только некоторые функции потребляют ресурсы в среде выполнения. Если не применять к ним средства предотвращения атак, можно сохранить производительность и максимально усилить влияние на безопасность.Вот примертакого подхода для одного из медиакодеков. Чтобы исключить риски, упомянутые функции следует предварительно проверить на наличие ошибок.
  • Оптимизировать использование средства предотвращения атак. Часто для этого необходимо внести изменения в компилятор. Например, наша команда перешла на использование IntegerOverflowSanitizer иBoundsSanitizer.
  • Параметры некоторых средств предотвращения атак, таких как встроенная устойчивость распределителя Scudo к уязвимостям в динамической памяти,можно настраиватьдля повышения производительности.

Для многих из этих улучшений необходимы изменения в проекте LLVM. В результате выигрывает не только платформа Android, но и другие участники сообщества LLVM.

Развертывание и поддержка


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

Влияние средств безопасности на стабильную работу системы


Важно понимать, возможно ли ложное срабатывание того или иного средства предотвращения атак. Например, если санитайзер Bounds выдает ошибку, речь точно идет о запрещенном доступе (хотя его могли и не использовать). В случае с санитайзером Integer Overflow возможны ложные срабатывания, так как целочисленное переполнение часто бывает абсолютно нормальным и безвредным процессом.

Поэтому важно учитывать влияние средств предотвращения атак на стабильность системы. Неважно, произошло ли ложное срабатывание или была реальная угроза безопасности, в любом случае пользователь испытывает неудобства. И здесь мы снова отмечаем, что необходимо четко понимать, для каких компонентов следует использовать те или иные средства безопасности. Потому что сбои в некоторых компонентах сильнее сказываются на стабильности системы. Если средство предотвращения атак вызывает сбой в медиакодеке, видео просто перестанет воспроизводиться. Однако в случае ошибки в процессеnetdпри установке обновления устройство может больше не включиться. Даже если для некоторых средств предотвращения атак ложное срабатывание не вызывает проблемы (например, как в случае с санитайзером Bounds), мы все равно проводим подробное тестирование, чтобы убедиться в стабильной работе устройства. Например, ошибки смещения на единицу могут не приводить к сбоям в обычном режиме, а санитайзер Bounds прерывает выполнение процесса и нарушает стабильную работу системы.

Также важно понимать, можно ли заранее выявить все компоненты, которые средство предотвращения атак может вывести из строя. Например, в случае с санитайзером Integer Overflow очень сложно предсказать риски, не проводя масштабного тестирования, потому что трудно определить, какие целочисленные переполнения являются намеренными (разрешенными), а какие могут вызвать уязвимости.

Поддержка


Необходимо рассматривать не только возможные проблемы при развертывании средств предотвращения атак, но и особенности их поддержки в долгосрочной перспективе. Мы оцениваем время, необходимое, чтобы интегрировать средство с существующими системами, активировать и отладить его, развернуть на устройствах, а затем обслуживать после запуска. Технология SELinux это хороший пример. Чтобы создать набор правил, требуется много времени и сил. И этот набор нужно поддерживать годами, невзирая на изменения кода, а также добавление или удаление отдельных функций.

Мы стремимся к тому, чтобы средства предотвращения атак минимально влияли на стабильность работы и чтобы у разработчиков была вся необходимая информация. Для реализации этих целей мы улучшаем текущие алгоритмы, чтобы уменьшить число ложных срабатываний, и публикуем документацию на страницеsource.android.com. Упростив отладку в случае сбоев, можно снизить нагрузку на разработчиков при обслуживании. Например, чтобы было проще обнаружить ошибки санитайзера UBSan, мы по умолчанию добавили в систему сборки Androidподдержкуминимального времени выполнения UBSan. Изначально минимальное время выполнения былодобавленодругими разработчиками Google специально для этой цели. При сбое программы из-за санитайзера Integer Overflow в сообщение об ошибке SIGABRT добавляется следующий фрагмент:

Abort message: 'ubsan: sub-overflow' 

Увидев это сообщение, разработчики поймут, что необходимовключить режим диагностики, чтобы распечатать информацию о сбое:

frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp:2188:32: runtime error: unsigned integer overflow: 0 - 1 cannot be represented in type 'size_t' (aka 'unsigned long')

При этом в SELinux есть инструмент audit2allow, который позволяет предлагать правила, разрешающие те или иные заблокированные операции:

adb logcat -d | audit2allow -p policy #============= rmt ============== allow rmt kmem_device:chr_file { read write };

И пусть audit2allow не всегда предлагает правильные варианты, он сильно помогает разработчикам, плохо знакомым с SELinux.

Заключение


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



Благодарим наших коллег и авторов: Кевин Деус, Джоэл Галенсон, Билли Лау, Иван Лосано специалистов по вопросам безопасности и конфиденциальности данных Android. Отдельная благодарность Звиаду Кардава и Джеффа Ван Дер Ступа за помощь в подготовке статьи.
Подробнее..

Перевод Как мы ускорили запуск приложения Dropbox для Android на 30

05.03.2021 14:13:31 | Автор: admin

Запуск приложения это первое впечатление наших пользователи после установки приложения. Это то, что происходит каждый раз. Простое и быстрое приносит пользователям гораздо больше радости, чем приложение, которое имеет массу функций, но требует вечности, чтобы запуститься. Команда Dropbox Android потратила время и силы на измерение, выявление и устранение проблем, влияющих на время запуска приложения. В итоге мы сократили время запуска приложения на 30 %, и вот история о том, как мы это сделали.


Страшный подъём

Исторически сложилось так, что в Dropbox мы отслеживали начало работы приложения, измеряя, сколько времени прошло с момента, когда пользователь нажал на иконку нашего приложения, и до момента, когда приложение полностью загрузилось и готово к работе с пользователем. Мы абстрагировали измерения инициализации приложения таким образом:

perfMonitor.startScenario(AppColdLaunchScenario.INSTANCE)// perform the work needed for launching the applicationperfMonitor.stopScenario(AppColdLaunchScenario.INSTANCE)

Как управляемая данными команда мы отслеживали и контролировали инициализацию приложения с помощью графиков, которые могли увидеть все инженеры. На графике ниже показаны измерения процентиля 90 при запуске приложения в период с конца марта по начало апреля 2020 года.

Как видно на графике, время запуска приложения, кажется, не так уж сильно меняется. Небольшие колебания при запуске приложений, которые варьируются в пределах пары миллисекунд, вполне ожидаемы в приложении, с которым на самых разных устройствах и ОС работают миллионы пользователей.

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

Хотя мы обнаружили медленно растущее время запуска случайно, замедление заставило нас углубиться в его причины, чтобы убедиться в том, что наши инструменты мониторинга, оповещения и графики дают широкий и целостный взгляд на метрики.

Больше цифр

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

  1. Выполнение миграции.

  2. Загрузка сервисов приложения.

  3. Загрузка первых пользователей.

Мы начали наше исследование с профилирования в Android Studio, чтобы измерить производительность наших тестовых телефонов. Проблема с профилированием производительности при таком подходе заключалась в том, что тестовые телефоны не давали статистически значимой выборки того, насколько хорошо на самом деле работает запуск приложения. Приложение Dropbox на Android имеет более 1 миллиарда установок на Google Play Store, охватывает несколько типов устройств, некоторые из них старые Nexus 5s, другие самые новые, лучшие устройства Google. Было бы глупо пытаться профилировать такое количество конфигураций. Поэтому мы решили измерить производительность разных этапов запуска приложения при помощи пошагового сценария в производственной среде.

Вот обновлённый код инициализации, где мы добавили логирование трёх упомянутых выше шагов:

perfMonitor.startScenario(AppColdLaunchScenario.INSTANCE)perfMonitor.startRecordStep(RunMigrationsStep.INSTANCE)// perform migrations step wrokperfMonitor.startRecordStep(LoadAppServicesStep.INSTANCE)// load application services stepperfMonitor.startRecordStep(LoadInitialUsers.INSTANCE)// perform initial user loading stepperfMonitor.stopScenario(AppColdLaunchScenario.INSTANCE)

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

Мы нашли виновников

На приведённом ниже графике показано общее время запуска приложения с января по октябрь 2020 года.

В мае мы начали измерять все основные этапы инициализации приложения. Эти измерения позволили нам выявить и устранить основных виновников замедления запуска приложения.

В проблемах с производительностью оказались виноваты инициализация библиотеки Firebase Performance, миграция флагов функциональности и начальная загрузка пользователя.

Firebase Performance Library

Чтобы измерять и отправлять метрики о производительности приложений, в Google Firebase включена Firebase Performance Library. Она предоставляет полезные функциональные возможности: показатели производительности отдельных методов, а также инфраструктуру для мониторинга и визуализации производительности различных частей приложения. К сожалению, библиотека производительности Firebase также имеет некоторые скрытые издержки. Среди них дорогостоящий процесс инициализации и, как следствие, значительное увеличение времени сборки. При отладке мы обнаружили, что инициализация Firebase Suite длилась в семь раз дольше, когда был включен инструмент Firebase Performance.

Чтобы устранить проблему с производительностью, мы решили удалить инструмент Firebase Performance из приложения Android Dropbox. Firebase Performance library замечательный инструмент, который позволяет профилировать детали производительности очень низкого уровня, такие как отдельные вызовы функций, но поскольку мы не использовали эти возможности библиотеки, то решили, что быстрый запуск приложения важнее данных о производительности отдельных методов, и поэтому удалили ссылку на эту библиотеку.

Миграции

Есть несколько внутренних миграций, выполняемых каждый раз при запуске приложения Dropbox. Они могут включать обновление флагов функций, миграцию баз данных и т. д. К сожалению, мы обнаружили, что некоторые из этих миграций запускались при каждом запуске приложения. Ранее мы не замечали их плохой производительности, потому что приложение быстро запускалось на устройствах разработки и тестирования. К сожалению, этот миграционный код особенно плохо работал на старых версиях ОС и старых устройствах, что способствовало увеличению времени запуска.

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

Загрузка пользователя

Мы храним метаданные контактов пользователей Dropbox на устройстве в виде больших двоичных объектов JSON это устаревшая часть приложения. В идеале эти большие двоичные объекты должны читаться и преобразовываться в объекты Java только единожды. К сожалению, код извлечения пользователей вызывался несколько раз из разных устаревших функций приложения, и каждый раз этот код выполнял дорогостоящий синтаксический анализ JSON, чтобы преобразовать кастомные объекты JSON в объекты Java. Хранение контактов пользователей в формате JSON как таковое было устаревшим решением в архитектуре и оно было частью легаси-монолита. Чтобы устранить эту проблему немедленно, мы добавили функциональность кэширования анализируемых пользовательских объектов во время инициализации. Мы продолжаем разбивать легаси-монолит, и более эффективным и современным решением для хранения контактов пользователей было бы использование объектов базы данных Room и преобразование этих объектов в бизнес-объекты.

Что делается сейчас?

В результате удаления ссылки на Firebase Performance и дорогостоящих шагов миграции, а также благодаря кэшированию пользовательских загрузок мы подняли производительность запуска приложений Dropbox Android на 30 %. Благодаря этой работе мы также собрали дашборды, которые помогут предотвратить деградацию времени запуска приложений в будущем.

Мы взяли на вооружение несколько методов, которые, мы надеемся, не позволят нам повторить ошибки с производительностью. Вот пара из них:

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

Обнаружив то, насколько библиотека Firebase Performance замедлила запуск нашего приложения, мы ввели добавление сторонних библиотек как процесс. Сегодня, прежде чем библиотеку можно будет добавить и использовать в нашей кодовой базе, нам необходимо измерить время запуска, время сборки и размер APK-файла приложения.

Мы всё кэшируем.

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

Заключение

Больше вкладывая в убедительную аналитику и измерения производительности, теперь команда ещё больше ориентируется на данные , что помогает нам вкладываться в кодовую базу, например убирать устаревший код C++ гораздо более уверенно. Сегодня мы следим за несколькими дашбордами, которые дают нам представление о производительности наиболее важных частей наших приложений, и у нас есть процессы, которые обеспечивают быстроту приложения Dropbox, чтобы оно радовало пользователей.


Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Другие профессии и курсы
Подробнее..

Как мы накосячили пока делали Бриллиантовый чекаут и что из этого вышло

17.02.2021 16:08:10 | Автор: admin

В начале 2019 мы собрали аналитику по адресам в заказах и так получилось, что большая часть клиентов заказывает доставку на одни и те же адреса. При этом они не устанавливают отложенное время. Получается, они хотят получить заказ туда же, куда и в прошлый раз и как можно быстрее.

И мы решили поменять флоу оплаты заказа и сделать его максимально простым именно для таких клиентов.

При оформлении заказа у нас есть два обязательных экрана: для выбора адреса и для выбора способа оплаты.

На экране адресов мы предвыбираем адрес, куда доставляли в прошлый раз. А на экране со способами оплаты предвыбираем то, чем оплачивали в прошлый раз.

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

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

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

Относим проблему дизайнерам.

Думаем: дизайн и обработка ошибок в приложении

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

Наш дизайнер Паша изучает задачу.Наш дизайнер Паша изучает задачу.

По ходу мы отмечали проблемные места.

В общем, вроде все кейсы взяли на карандашик. Среди них автоматические акции, применение промокода одного города в другом и актуальное меню.

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

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

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

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

Стало ясно, что чекаут без геолокации делать будет сложно. Ну, точнее, наличие гео решило бы большую часть проблем: актуальное меню, отсутствующие позиции, промокоды.

А требование сделать без геолокации. Потому что её совсем нет, а делать с нуля долго и сложно: это затрагивает не только приложение, но и наш монолит.

Первая мысль

Первая мысль дизайнера Паши воркэраунд: спрашивать адрес, на который делать доставку, в первую очередь. Этакая геологация без доступа к геолокации. Сергей, наш тогдашний CPO, эту идею забрил. Сказал, что это лишний шаг (Серёжа, прости, если мы твои слова переврали). Из-за этого пришлось расписывать больше кейсов, особенно разные ошибочные состояния.

Но, как это иногда бывает в гибкой компании, приоритеты вдруг поменялись. И наши проблемы и задачи оказались уже не такими важными, как другие.

Пришлось задвинуть новый чекаут далеко и надолго.

Переключаемся на кастомизируемые комбо

Вместо чекаута мы взялись за новую задачу кастомизируемые комбо. Это когда ты сам выбираешь какая пицца, десерт или напиток входят в твой набор. Задача непростая, требует много разработчиков и времени. Делали несколько месяцев, старались успеть к федеральной рекламе. В итоге мы чуть-чуть не успели и вместо федеральной рекламы запустили таргетированную в интернете.

Переключаемся на Приложение в ресторане

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

Снова все вместе сели, подумали, обрисовали разные кейсы. И поняли, что для Приложения в ресторане нужна максимально удобная оплата, да и просто хочется оплату покрасивше, посексуальнее в конце концов. Чтобы клиент захотел заказывать и оплачивать через приложение, а не через кассира.

Ну, привет, Бриллиантовый Чекаут. Мы возвращаемся к тебе.

Рисуем: 12 макетов на одно и то же

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

Примеры макетов с разных подходов и итераций.Примеры макетов с разных подходов и итераций.

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

http://personeltest.ru/aways/www.scotthurff.com/posts/how-to-design-for-thumbs-in-the-era-of-huge-screens/https://www.scotthurff.com/posts/how-to-design-for-thumbs-in-the-era-of-huge-screens/

Мы работаем во многих странах и везде свои особенности: где-то есть додо-рубли, где-то нет,где-то есть электронные чеки, где-то нет. А в Британии так вообще город можно сменить на экране оплаты. Все эти проблемы приходилось обдумывать и решать.

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

Разрабатываем и ошибаемся

Мы оценили разработку нового чекаута в 2 месяца, а закончили через 9. И вот почему.

Начали со сложного дизайна

Изначально макеты были обильно закастомайзены. В целом не было ничего нерешаемого, но в любом случае это требовало дополнительного времени. Самый яркий пример кастомный транзишн выезжающей снизу шторки.

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

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

Решение: начинать с простого дизайна и усложнять итеративно. Сели делать задачу, получили рабочую механику вот теперь можно немного закастомайзить дизайн. А потом ещё сильнее. И ещё. А для этого надо привлекать разработчиков с самого раннего этапа отрисовки макетов.

Меняли дизайн на ходу

Иногда смотрели на макеты, прикидывали поведение экранов, окали и шли делать. А потом, спустя время, понимали, что так-то в целом можно сделать и получше. Шли к дизайнеру, обсуждали, переделывали.

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

Переиспользовали код, когда не нужно было

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

Разумеется, там старое непротестированное легаси. Разумеется, туда приходилось вкорячивать ифчики. А потом вообще оказалось, что этот экран строит ещё и вьюмодели для списка пиццерий. А в Москве их 200+ и это нифига не быстро. В конечном итоге поняли, что новый экран со всеми его зависимостями было бы написать быстрее и надежнее: тесты не просто получилось бы написать, так ещё и писать было бы гораздо быстрее.

Решение: не рассчитывать, что переиспользование кода ускорит разработку часто требуется рефактор, а это время. Особенно, если код не был покрыт тестами в таком случае его не получится менять безопасно.

Взяли в команду менторов и новичков

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

Так получилось, что оба новичка уволились ну, бывает. Для андройда это не было критичным, а на иосе пришлось звать на помощь человека из соседней команды. Он уже был знаком с проектом, но не был знаком с нашим модулем. Опять погружать в код.

Решение: не допускать новичков и менторов к разработке фич с горячими дедлайнами. Ну или при планировании закладывать время на онбординг. Кстати, про онбординги у нас тоже есть статья.

Недостаточно точно описывали таски

Один разработчик заболел, его задачи пришлось передать другому. При передаче несколько требований пропустили. А когда таски получили статус Готово готово было не то что нужно. Пришлось переделывать.

Решение: детальнее описывать требования к задачам. В каждой карточке должен быть чеклист того, что надо сделать. За ним надо следить, его надо актуализировать. Ну или конвертировать в отдельные карточки, если чувствуете, что надо ещё подекомпозировать.

Обсуждали одни и те же места по нескольку раз

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

Решение: копить причины принятых решений.

Неправильно оценили сроки

Оценивали сроки, сейчас не шучу, чуть ли не на глаз. Код мест, с которыми пришлось бы работать, даже не открывали. Ну а че, мы уже ого как долго в проекте, все умные и красивые и так сможем! Оказалось, что нет.

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

Так дольше, но оценка будет честнее.

Решили сэкономить на тестах

Начали резать скоуп, когда поняли, что не успеваем: минус там, минус тут. Под нож попали и тесты. В итоге иногда что-то отпадало, приходилось чинить. Иногда отпадало снова и снова. Классика.

Из-за то, что чекаут затаскивался за фича-флагом, а между старым и новым есть пересекающиеся места, что-то ломалось и в старом чекауте. Приходилось следить сразу за двумя модулями.

Решение: не отказываться от тестов, даже если сроки горят. Код без тестов легаси. Без тестов ты выигрываешь пару дней/недель сегодня, но проигрываешь месяцы в будущем.

Не пошарили знания

У нас есть CI. И, конечно же, его кто-то поддерживает. Со стороны иоса на тот момент это был я. Но одновременно с этим я занимался и разработкой нового чекаута.

К сожалению, из-за изменений в архитектуре проекта наш CI какое-то время был нестабильным. И это стопало разработку у остальных команд QA не могли получить актуальные сборки и проверить новые фичи или починенные баги.

Поэтому его приходилось чинить. А так как я не успел на тот момент пошарить знания чинить приходилось именно мне, в ущерб разработке чекаута.

Решение: Активнее шарить знания на встречах или во внутренней документации.

Самое главное решение

Не допускать релиза крупных фичей разом, делать всё итеративно.

Так инкремент можно получать гораздо раньше. А не раз в год, как получилось у нас.

Re: Подводим итоги Final_v3

Спустя год разработки наконец-то доталкиваем новый чекаут до релиза. Сначала на Россию, здесь 90% заказов. А потом и на остальные страны, после разных допиливаний: додо-рубли, электронные чеки и прочее, о чем рассказывал выше.

Полезли в аналитику сравнивать конверсию, А ТАМ ТАКОЕ. Цифры просто космические: миллионы посыпались на нас сверху, разработка всего чекаута окупилась буквально за неделю. Потому что конверсия выросла аж на 5%!

А потом поняли, что аналитика кривая. Собрали новую и увидели, что конверсия выросла только на 0,5%. В целом неплохо, но хотелось чуть получше.

Подумали, посовещались, посоветовались и собрали аналитику в третий раз. На этот раз точно, железобетонно и финально: конверсия выросла на 1,5%. В рублях это дополнительные 2 000 000 в неделю.

Работаем над ошибками

БЧ сдан. Возвращаемся к Приложению в Ресторане.

  • Тратим на оценку задачи несколько дней.

  • Декомпозируем до посинения.

  • Постоянно смотрим в код, включаем в оценку время тесты.

  • На аналитику время заложили.

  • И на тестирование тоже.

  • И на возможные баги с прода.

  • И ещё всякого по мелочи.

Составили, в общем, самый настоященский и максимально проработанный роадмап.

И релизнули фичу день в день с планом.

Вот такой вот хеппи енд


Также будет интересно почитать:

Подписывайтесь начат Dodo Engineering, если хотите обсудить эту и другие наши статьи и подходы, а также на каналDodo Engineering, где мы постим всё, что с нами интересного происходит.

А если хочешь присоединиться к нам в Dodo Engineering, то будем рады сейчас у нас открыты вакансииiOS-разработчиков(а ещё для Android, frontend, SRE и других). Присоединяйся, будем рады!

Подробнее..

Дайджест интересных материалов для мобильного разработчика 382 (15 21 февраля)

21.02.2021 14:07:08 | Автор: admin
В этом выпуске цвета Swift, переиспользуемый чистый Kotlin, выход первой версии Android 12 и страсти по IDFA, дефекты Qt и бриллиантовый чекаут, секреты маркетинга приложений, игровые боты, знания за 5 минут и многое другое.



Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

Предотвращаем мерж-конфликты с XcodeGen
Цвета в Swift: UIColor
Распознание блоков текста в iOS-приложении с помощью Vision
Apple начала бороться с иррационально высокими ценами в приложениях?
Забанила ли Apple аналитические SDK? Ээ ну
Взлом нативных двоичных файлов ARM64 для запуска на симуляторе iOS
Погружение в CFRunLoop
Создайте новостное приложение в SwiftUI 2.0 (Combine, API, MVVM & Swift Package Manager)
Используем Charles для переписывания ответов при разработке приложений для iOS
Clubhouse-подобное изображение в профиле на Swift
Создаем анимированные круговые и кольцевые диаграммы в SwiftUI
Создание рулетки на SwiftUI
OnTap: документация по SwiftUI
WatchLayout: круги в UICollectionView
SPAlert: уведомления в стиле Apple

Android

Как писать и переиспользовать код на чистом Kotlin. Заметки Android-разработчика
Как найти подходящую абстракцию для работы со строками в Android
Темы, стили и атрибуты
Вышла превью-версия Android 12
GitHub Actions для Android-разработки
Как мы ускорили запуск приложения Dropbox для Android на 30%
Как изменится дизайн в Android 12
Контрольный список качества приложения
Анти-паттерны RecyclerView
StateFlow с одно- и двусторонним DataBinding-ом на Android
Как на самом деле работает RxJava
Готовим наши приложения к Jetpack Compose
Простое создание параллакса на Jetpack Compose
5 расширений Kotlin, которые сделают ваш Android-код более выразительным
IridescentView: переливающиеся изображения для Android
stackzyr: Jetpack Compose для десктопов

Разработка

Обработка дат притягивает ошибки или 77 дефектов в Qt 6
Запуск топ-приложения в одиночку, бесплатно и без кодинга (ну почти)
Как мы накосячили пока делали Бриллиантовый чекаут 9 месяцев, а планировали 2
1 год с Flutter в продакшне
Тесты должна писать разработка (?)
Опыт разработки первой мобильной игры на Unity или как полностью перевернуть свою жизнь
О поиске утечек памяти в С++/Qt приложениях
Стратегия тестирования краткосрочного проекта
Готовим Большую Фичу на Kotlin Multiplatform. Доклад Яндекса
ZERG что за зверь?
Podlodka #203: платежи
Microsoft открывает Dapr для простого развертывания микросервисов
Задачи с собеседований: 2 в 64 степени
Дизайн приложений: примеры для вдохновения #32
Как сделать инсайты UX-исследований видимыми, прослеживаемыми и увлекательными?
5 вопросов на интервью для выявления выдающихся программистов
Как создать простое шахматное приложение с помощью Flutter
Создавая бэкенд Uber: пошаговое руководство по системному дизайну
5 удивительных преимуществ обмена знаниями в качестве разработчика
Чтение кода это навык
Почему я перестал читать статьи Как стать разработчиком программного обеспечения
Психология дизайна и нейробиология, стоящая за классным UX
Удаленное определение частоты пульса с помощью веб-камеры и 50 строк кода
Как разозлить разработчика
7 обязательных навыков, чтобы стать выдающимся разработчиком

Аналитика, маркетинг и монетизация

Кратко о продуктовых метриках
Маркетологи в мобайле: Денис Нуждин (Пятёрочка Доставка)
Секреты маркетинга приложений для знакомств новое руководство Adjust
Среда совместного программирования Replit получила $20 млн
Photomath получил еще $23 млн.
Post-IDFA Alliance открыл сайт Нет IDFA? Нет проблем
Взрослые в США в 2020 прибавили сразу час цифрового времени
ВКонтакте запустил новый инструмент для автоматизированной рекламы приложений
Отчет Состояние рынка приложений для фитнеса и здоровья 2021
Jigsaw получает $3.7 млн на дейтинг с головоломкой
Uptime: знания за пять минут
Как запустить wellness-стартап на свои деньги, совмещать с постоянной работой и не сойти с ума
Что будет с трекингом мобильных приложений в 2021 году
Новая норма: обучение в приложениях и как добиться успеха в меняющиеся времена
Лучшие маркетинговые метрики для отслеживания показателей роста
Вот почему разработчикам не удается добиться успеха в карьере
Как я занимался маркетингом своей игры, продажи которой за год составили 128 тысяч долларов

AI, Устройства, IoT

Cчетчик газа в Home Assistant без паяльника
Устройство игрового бота: 16-е место в финале Russian AI Cup 2020 (и 5-е после)
Умный дом с нуля своими руками или путешествие длиною в год
Как распознать рукописный текст с помощью ИИ на микроконтроллерах
Часы для обнаружения жестов на основе машинного обучения, ESP8266 и Arduino
Как преобразовать текст в речь с использованием Google Tesseract и Arm NN на Raspberry Pi
Быстрый прототип IIoT-решения на Raspberry PI и Yandex IoT. Часть вторая
Первый опыт с Raspberry Pi или микросервисы для дома
Google сворачивает Swift для TensorFlow

< Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку пришлите, пожалуйста, в почту.
Подробнее..

Стоп рефакторинг. Kotlin. Android

24.02.2021 00:19:22 | Автор: admin

Введение

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

Я хочу рассказать про практики, которые не один раз уже выручали нас в проекте. Подборка примеров получилась не на пустом месте, все реальные примеры PullRequest-ов.
Все примеры НЕ выдуманные и тестировались на живых людях. В процессе сбора данных несколько людей пострадало.

Заменяйте if-else на when где это необходимо

Долгое время Java был предпочтительным языком программирования для платформы Android. Затем на арену пришел Kotlin, да вот привычки остались старые.

fun getNumberSign(num: Int): String = if (num < 0) {    "negative"} else if (num > 0) {    "positive"} else {    "zero"}

Красиво - 7 строк и получаем результат. Можно проще:

fun getNumberSign(num: Int): String = when {    num < 0 -> "negative"    num > 0 -> "positive"    else -> "zero"}

Тот же код, а строк 5.

Не забываем и про каскадное использованиеif-elseи его нечитабильность при разрастании кодобазы. Если в вашем проекте нет необходимости поддерживать 2 ЯП(Kotlin + Java), настоятельно рекомендую взять его себе на вооружение. Одна из самых популярных причин его игнорирования - "Не привычно"

Дело не в предпочтениях стилистики писания: семистопный дактиль или пятистопный хорей. Дело в том, что в Kotlin отсутствует операторelse-if. Упуская этот момент можно выстрелить себе в ногу. А вот и сам пазлер 9 отАнтона Кекса.

Я не рекомендую использоватьwhenвезде, где только можно. В Kotlin нет(и небудет) тернарного оператора, и стандартные булевы условия стоит использовать по классике. Когда условий больше двух, присмотритесь и сделайте код элегантнее.

Отряд булевых флажков

Рассмотрим следующее на примере поступающего ТЗ в динамике:

1. Пользователь должен иметь возможность видеть доставлено сообщение или нет

data class Message(  // ...  val isDelivered: Boolean)

Все ли здесь хорошо? Будет ли модель устойчива к изменениям? Есть ли гипотетическая возможность того, что в модели типаMessageне будут добавлены новые условия в будущем? Имеем ли мы право считать, что исходные условия ТЗ есть оконченный постулат, который нельзя нарушить?

2. Пользователь должен иметь возможность видеть прочитано сообщение или нет

data class Message(  // ...  val isDelivered: Boolean,  val isRead: Boolean) 

Не успели мы моргнуть глазом, как ProductOwner передумал и внес изменения в первоначальные условия. Неожиданно? Самое простое решение - добавить новое поле и "решить" проблему. Огорчу, не решить - отложить неизбежное. Избавление от проблемы здесь и сейчас - must have каждого IT инженера. Предсказание изменений и делать устойчивую систему - опыт, паттерны, а иногда, искусство.

Под "отложить неизбежное" я подразумеваю факт того, что рано или поздно система станет неустойчива и придет время рефакторинга. Рефакторинг -> дополнительное время на разработку -> затраты не по смете бюджета -> неудовлетворенность заказчика -> увольнение -> депрессия -> невозможность решить финансовый вопрос -> голод -> смерть. Все из-за Boolean флага?!!! COVID-19 не так уж страшен.

Что не так? Сам факт появления изменений не есть глупость PO, который не мог сразу сформулировать свою мысль. Не все то, что очевидно сейчас, было очевидно ранее. Чем меньше время на маневр, тем вы ценнее и конкурентнее. Далее включим фантазию и попробуем предугадать, что же еще может придумать менеджер?

3. Пользователь должен иметь возможность видеть отправлено ли сообщение

4. Пользователь должен иметь возможность видеть появилось ли сообщение в нотификациях e.t.c.

Если мы сложим воедино все новые требования, будет видно, что объектMessageможет находиться только в одном состоянии: отправлено, доставлено, появилось ли сообщение в нотификациях, прочитано Набор состоянийдетерминирован. Опишем их и заложим в наш объект:

data class Message(  // ...  val state: State) {    enum class State {        SENT,        DELIVERED,        SHOWN_IN_NOTIFICATION,        READ    }}

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

data class Message(  // ...  val states: Set<State>) {  fun hasState(state: State): Boolean = states.contains(state)}// либо data class Message(    // ...    val states: States) {    enum class State(internal val flag: Int) {        SENT(1),        DELIVERED(1 shl 1),        READ(1 shl 2),        SHOWN_IN_NOTIFICATION(1 shl 3)    }    data class States internal constructor(internal val flags: Int) {        init {          check(flags and (flags+1)) { "Expected value: flags=2^n-1" }        }        constructor(vararg states: State): this(            states.map(State::flag).reduce { acc, flag -> acc or flag }        )        fun hasState(state: State): Boolean = (flags and state.flag) == state.flag    }}

Выводы: перед тем как начать проектировать систему, задайте необходимые вопросы, которые помогут вам найти подходящее решение.Можно ли считать набор условий конечным? Не изменится ли он в будущем?Если ответы на эти вопросы ДА-ДА - смело вставляйте булево состояние. Если же хоть на один вопрос ответ НЕТ - заложите детерменированный набор состояний. Если объект в один момент времени может находиться в нескольких состояниях - закладывайте множество.

А теперь посмотрим на решение с булевыми флагами:

data class Message(  //..  val isSent: Boolean,  val isDelivered: Boolean  val isRead: Boolean,  val isShownInNotification: Boolean) //...fun drawStatusIcon(message: Message) {  when {    message.isSent && message.isDelivered && message.isRead && message.isShownInNotification ->     drawNotificationStatusIcon()    message.isSent && message.isDelivered && message.isRead -> drawReadStatusIcon()    message.isSent && message.isDelivered -> drawDeliviredStatusIcon()    else -> drawSentStatus()   }}

Попробуйте добавить еще одно состояние(ошибку) в конец и в середину списка приоритетов. Без чтения документации и без ознакомления работы с флагами это сделать будет проблематично.

Одно состояние

Одно состояние описывается несколькими независимыми переменными. Редкая проблема, которая открывается при потере фокуса над контекстом разрабатываемого компонента.

data class User(    val username: String?    val hasUsername: Boolean)

По условию контракта есть возможность не заполнить имя пользователя. На GUIне такое состояние должно подсветиться предложением. За состояние предложения, логично считать, переменнуюhasUsername. По объявленным соглашениям, легко допустить простую ошибку.

// OKval user1 = User(username = null, hasUsername = false) // Ошибка, имя пользователя естьval user2 = User(username = "user", hasUsername = false) // OKval user3 = User(username = "user", hasUsername = true) // Ошибка, имя пользователя не задано, а флаг говорит об обратномval user4 = User(username = null, hasUsername = true) // Ошибка, имя пользователя пустое, а флаг говорит об обратномval user5 = User(username = "", hasUsername = true) // Ошибка, имя пользователя пустое, а флаг говорит об обратномval user6 = User(username = " ", hasUsername = true) 

Узкие места в контракте открывают двери для совершения ошибки. Источником ответственности за наличие имени является только одно поле -username.

data class User(    val username: String?) {    fun hasUsername(): Boolean = !username.isNullOrBlank()}

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

  • вычислить сразу либо заленивить состояние

data class User(    val username: String?) {    val hasUsername: Boolean = !username.isNullOrBlank()    val hasUsernameLazy: Boolean by lazy { !username.isNullOrBlank() }}
  • вынести вычисление в утилитарный класс. Используйте только в случае тяжеловесности операции

class UsernameHelper {    private val cache: MutableMap<User, Boolean> = WeakHashMap()        fun hasUsername(user: User): Boolean = cache.getOrPut(user) {       !user.username.isNullOrBlank()     }}

Абстракции - не лишнее

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

Ключи для 3rd party services получаем из backend. Клиент долженсохранитьэти ключи для дальнейшего использования в приложении.

// ...val result = remoteService.getConfig()if (result is Result.Success) {  val remoteConfig = result.value.clientConfig?.keys  for (localConfigKey: ConfigKey in configKeyProvider.getConfigKeys()) {    sharedPreferences.edit { putString(localConfigKey.key, remoteConfig[localConfigKey.key]) }    }}//...enum class ConfigKey(val key) {  FACEBOOK("facebook"),  MAPBOX("mapbox"),  THIRD_PARTY("some_service")}

Спустя N недель получаем предупреждение от службы безопасности, что ключи сервисаTHIRD_PARTYни в коем случае нельзя хранить на диске устройства. Не страшно, можем спокойно хранить ключи хранить InMemory. И по такой же стратегии нужно затронуть еще 20 компонентов приложения. Хм, и как поможет абстракция?

Завернем под абстракцию хранлище ключей и создадим имплементацию: InMemory / SharedPreferences / Database / WeakInMemory А дальше с помощью внедрения зависимостей. Таким образом мы не нарушимSOLID - в нашем примере актором будет являться алгоритм сбора данных, но не способ хранения; open-closed principle достигается тем, что мы "прикрываем" необходимость модификации алгоритма за счет абстракции.

// ...val result = remoteService.getConfig()if (result is Result.Success) {  val remoteConfig = result.value.clientConfig?.keys  for(localConfigKey: ConfigKey in configKeyProvider.getConfigKeys()) {    configurationStorage.put(        configKey = localConfigKey,         keyValue = remoteConfig[localConfigKey.key]      )  }}//....interface ConfigKeyStorage {   fun put(configKey: ConfigKey, keyValue: String?)   fun get(configKey: ConfigKey): String   fun getOrNull(configKey: ConfigKey): String?}internal class InMemoryConfigKeyStorage : ConfigKeyStorage {private val storageMap: MutableMap<ConfigKey, String?> = mutableMapOf()  override fun put(configKey: ConfigKey, keyValue: String?) {    storageMap[configKey] = keyValue}  override fun get(configKey: ConfigKey): String =       requireNotNull(storageMap[configKey])override fun getOrNull(configKey: ConfigKey): String? =       storageMap[configKey]}

Если помните, в изначальной постановке задачи не стояло уточнение о типе хранилища данных. Подготавливаем систему к изменениям, где имплементация может быть различной и никак не влияет на детали алгоритма сбора данных. Даже если в изначальных требованиях и были бы уточнения по типу хранилища - это повод для того, чтобы усомниться и перестраховаться. Вместо того, чтобы влезать в N компонентов для модификации типа хранилища, можно добиться этого с помощью замены источника данных через DI/IoC и быть уверенным, что все работает исправно. Так же, такой код проще тестировать.

Описывайте состояния явно

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

В очередной раз возьмем пример технического задания:

Подготовить репозиторий для вывода имени пользователя на экран

Создадим репозиторий, который будет возвращать имя пользователя. Выведемnullв случае, если не смогли получить имя. Так как в первоначальном задании не шло речи о том, откуда нам нужно брать данные - оставим дело за абстракцией и заодно создадим наивное решение для получения из remote.

interface UsernameRepository {    suspend fun getUsername(): String?}class RemoteUsernameRepository(    private val remoteAPI: RemoteAPI) : UsernameRepository {    override suspend fun getUsername(): String? = try {        remoteAPI.getUsername()    } catch (throwable: Throwable) {        null    }}

Мы создали контракт получения имени пользователя, где в качестве успeшного результата приходит состояниеString?и в случае провала полученияString?. При чтении кода, нет ничего подозрительного. Мы можем определить состояние ошибки простым условиемgetUsername() == nullи все будут счастливы. По факту, мы не имеем состояния провала. По контрактуSuccessState === FailState.

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

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

interface UsernameRepository {    suspend fun getUsername(): String?}class CommonUsernameRepository(  private val remoteRepository: UsernameRepository,  private val localRepository: UsernameRepository) : UsernameRepository {    suspend fun getUsername(): String? {        return remoteRepository.getUsername() ?: localRepository.getUsername()    }}

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

  • верно ли утверждать, что результатnull- имя пользователя? Обязательных условий мы не имеем. Все легально.

  • верно ли утверждать, что результатnull- состояние из кэша?

  • верно ли утверждать, что результатnull- состояние ошибки удаленного узла при пустом кэше?

Однозначного ответа нет, так как наш контракт не подразумевает этого. Да и по требованиям от нас этого не требуют. А зачем нужно явно декларировать состояния для системы, если важен результат? Смежные состояния при одинаковых результатах - русло неустойчивости узла к изменениям. Избегайте неоднозначности. Новые требования, которые затронут хотя бы один смежный статус, потребуют рефакторинга корневой модели. Изменения в начальной абстракции - это очень страшно. Это чревато тем, что вам придется переписать все известные имплементации.

В случае получения ошибки - изменить цвет имени на экране.

Используйтеenum/sealed classes/interfaces/abstract classes. Техника выведения абстракций зависит от изначальных условий проекта. Если вам важна строгость в контрактах и вы хотите закрыть возможность произвольного расширения -enum/sealed classes. В противном случае -interface/abstract classes.

sealed class UsernameState {data class Success(val username: CharSequence?) : UsernameState()  object Failed : UsernameState()}

When может не хватить

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

enum class NavigationFlow {  PIN_CODE,  MAIN_SCREEN,  ONBOARDING,  CHOOSE_LANGUAGE}fun detectNavigationFlow(): NavigationFlow {    return when {        authRepo.isAuthorized() -> NavigationFlow.PIN_CODE        languageRepo.defaultLanguage != null -> NavigationFlow.CHOOSE_LANGUAGE        onboardingStorage.isCompleted() -> NavigationFlow.MAIN_SCREEN        else -> NavigationFlow.ONBOARDING    }}

Мы определили возможные состояния навигации. Так проще будет реализовать навигатор. Но вотdetectNavigationFlowстал слишком много знать. Разломать функцию могут следующие события: добавить новое состояние, изменить приоритет или когда используемый репозиторий станет устаревшим Постараемся создать такую функцию, в которой основной участок будет неизменен, а шаги можно будет легко заменить.

enum class NavigationFlow {    PIN_CODE,    MAIN_SCREEN,    ONBOARDING,    CHOOSE_LANGUAGE}// Описываем возможные состояния явноsealed class State {    data class Found(val flow: NavigationFlow) : State()    object NotFound : State()}interface NavigationFlowProvider {    // Возвращаем не null NavigationFlow чтобы гарантировать проход на следующий экран    fun getNavigation(): NavigationFlow}// Абстракция для поиска подходящего флоу для навигацииinterface NavigationFlowResolver {    fun resolveNavigation(): State}internal class SplashScreenNavigationFlowProvider(    // Sequence - для того чтобы прервать итерации при нахождении первого подходящего условия.    // Обратите внимание на очередность экземляров класса в последовательности.    private val resolvers: Sequence<NavigationFlowResolver>) : NavigationFlowProvider {    override fun getNavigation(): NavigationFlow = resolvers        .map(NavigationFlowResolver::resolveNavigation)        .filterIsInstance<State.Found>()        .firstOrNull()?.flow        // Если ничего не нашли - проход в состояние неизвестности        ?: NavigationFlow.MAIN_SCREEN}

Заменяем N-условныйwhenнаChainOfResponsibililty. На первый взгляд выглядит сложным: кода стало больше и алгоритм чуть сложнее. Перечислим плюсы подхода:

  1. Знакомый паттерн из ООП

  2. Соответствует правилам SOLID

  3. Прост в масштабировании

  4. Прост в тестировании

  5. Компоненты резолвера независимы, что никак не повлияет на структуру разработки

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

Наследование или композиция

Вопрос по этой теме поднимался ни один миллионраз. Я не буду останавливаться на подробностях, детали о проблемах можете почитать на просторах google. Хочу затронуть тему платформы, когда причина избыточного использования наследования - "платформа". Разберем на примерах компонентов Android.

BaseActivity. Заглядывая в старые прокты, с ужасом наблюдаю, какую же ошибку мы допускали. Под маской повторного использования смело добавляли частные случаи в базовую активити. Шли недели, активити обрастали общими прогрессбарами, обработчиками и пр. Проходят месяцы, поступают требования - на экране N прогрессбар должен отличаться от того, что на всех других От общей активити отказаться уже не можем, слишком много она знает и выполняет. Добавить новый прогрессбар как частный случай - выход, но в базовом будет оставаться рудимент и это будет нечестное наследование. Добавить вариацию вBaseActivity- обидеть других наследников и Через время вы получаете монстра в > 1000 строк, цена внесения изменений в который слишком велика. Да и не по SOLID это все.

Агаок, но мне нужно использовать компоненту, которая точно будет на всех экранах кроме 2х. Что делать?

Не проблема, Android SDK еще с 14 версиипредоставили такую возможность.Application.ActivityLifecycleCallbacksоткрывает нам простор на то, чтобы переопределять элементы жизненного цикла любойActivity. Теперь общие случаи можно вынести в обработчик и разгрузить базовый класс.

class App : Application(), KoinComponent {    override fun onCreate() {        super.onCreate()        // ...         registerActivityLifecycleCallbacks(SetupKoinFragmentFactoryCallbacks())    }    // Подключаем Koin FragmentFactory для инициализации фрагментов с помощью Koin    private class SetupKoinFragmentFactoryCallbacks : EmptyActivityLifecycleCallbacks {        override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {            if (activity is FragmentActivity) {                activity.setupKoinFragmentFactory()            }        }    }}

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

abstract class BaseActivity(@LayoutRes contentLayoutId: Int = 0) : AppCompatActivity(contentLayoutId) {    // attachBaseContext по умолчанию protected    override fun attachBaseContext(newBase: Context) {        // добавляем extension для изменения языка на лету        super.attachBaseContext(newBase.applySelectedAppLanguage())    }}

BaseFragment. С фрагментами все тоже самое. ИзучаемFragmentManager, добавляемregisterFragmentLifecycleCallbacks- профит. Чтобы проброситьFragmentLifecycleCallbacksдля каждого фрагмента - используйте наработки из предыдущих примеров сActivty. Пример на базе Koin -здесь.

Композиция и фрагменты. Для передачи объектов можем использовать инъекции DIP фреймворков - Dagger, Koin, свое и т.д. А можем отвязаться от фрейморков и передать их в конструктор. ЧТОООО? Типичный вопрос с собеседования - Почему нельзя передавать аргументы в конструктор фрагмента? До5 ноября 2018 года было именно так, теперь же естьFragmentFactoryи это стало легально.

BaseApplication. Здесь чуть сложнее. Для разныхFlavorsиBuildTypeнеобходимо использовать базовыйApplicationдля возможности переопределения компонентов для других сборок. Как правило,Applicationстановится большим, потому что на старте приложения, необходимо проинициализировать большое количество 3rd party библиотек. Добавим к этому и список своих инициализаций и вот мы на пороге того момента, когда нам нужно разгрузить стартовую точку.

interface Bootstrapper {    // KoinComponent - entry point DIP для возможности вызвать инъекции зависимостей в метод     fun init(component: KoinComponent)}interface BootstrapperProvider {    fun provide(): Set<Bootstrapper>}class BootstrapperLauncher(val provider: BootstrapperProvider) {    fun launch(component: KoinComponent) {        provider.provide().onEach { it.init(component) }    }}class App : Application() {  override fun onCreate() {        super.onCreate()        // Вызываем бутстраппер после инициализации Koin        this.get<BootstrapperLauncher>().launch(component = this)    }}

Разгружаем килотонны методов в разныеBootstrapperинстансы и делаем наш код чище. Либо можем воспользоваться нативным решением отзеленого робота.

Уменьшение области видимости

Инкапсуляция - один из немаловажных моментов в ООП парадигме. Современные языки программирования не просто так содержат в себе модификаторы доступа, которые ограничивают скоуп видимости сигнатур. Уменьшение простора на использование строк кода поддержано на уровне компилятора. Это отличная защита от (дурака)того, что код изначально написан так, чтобы его нельзя было модифицировать. В противном случае, он не скомпилируется. На практике встречаются случаи, когда ограничения касаются только внутренних состояний и поведений объекта - приватные функции, а что насчет самого объекта?

Выделим отдельный модуль, который будет содержать в себе объекты валидации входящих состояний.

interface Validator {    fun validate(contact: CharSequence): ValidationResult}sealed class ValidationResult {    object Valid : ValidationResult()    data class Invalid(@StringRes val errorRes: Int) : ValidationResult()}class PhoneNumberValidator : Validator {    override fun validate(contact: CharSequence): ValidationResult =        if (REGEX.matches(contact)) ValidationResult.Valid         else ValidationResult.Invalid(R.string.error)    companion object {        private val REGEX = "[0-9]{16}".toRegex()    }}

А разве плохо иметь публичный класс, который будет доступен всем? Но избыточное использование публичных сущностей по умолчанию означает, что объект данного класса может использоваться каждым. Возникает желание внести изменения для личных нужд не задумываясь о последствиях. Если вы не обезопасились методами, которые не пропустят "сломанный" код в рабочую среду, ждите бага.

Пришло обновление задачи, когда на экране N вместоMSISDNнеобходимо использоватьE.164:

class PhoneNumberValidator : Validator {    override fun validate(contact: CharSequence): ValidationResult =        if (REGEX.matches(contact)) ValidationResult.Valid         else ValidationResult.Invalid(R.string.error)    companion object {        private val REGEX = "+[0-9]{16}".toRegex()    }}

Да вот беда, такое наглое вмешательство в дела валидации разломало наше старое поведение на других экранах ввода, так репозитории не были готовы принять знак плюса в начале строки. Можно было бы обойтись документацией, которая бы описывала поведение и последствия изменения. Однако, документацию читают не всегда. Она не дает строгости использования.

С одной стороны, проблема надуманная и обойти ее можно было:

  • создать новый валидатор

  • создать валидатор с регексом по умолчанию и передать аргумент для частного случая

  • наследование и переопределение

  • другой подход

А теперь, давайте посмотрим на код, если бы мы изначально забетонировали MSISDN валидатор и вынесли бы его в бинарь.

interface Validator {    fun validate(contact: CharSequence): ValidationResult}sealed class ValidationResult {    object Valid : ValidationResult()    data class Invalid(@StringRes val errorRes: Int) : ValidationResult()}internal class MSISDNNumberValidator : Validator {//... код выше}internal class E164NumberValidator : Validator {//... код выше}

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

interface ValidatorFactory {    fun create(type: ValidatorType): Validator?    interface ValidatorType    companion object {        fun create() : ValidatorFactory {            return DefaultValidatorFactory()        }    }}object MSISDN : ValidatorFactory.ValidatorTypeobject E164 : ValidatorFactory.ValidatorTypeprivate class DefaultValidatorFactory : ValidatorFactory {    override fun create(type: ValidatorFactory.ValidatorType): Validator? = when(type) {        is MSISDN -> MSISDNValidator()        is E164 -> E164Validator()        else -> null    }}

Собираем кубик, пакуем в бинарь и отдаем в использование. По мере необходимости код открыт для расширения, но закрыт для модификации. Если вы захотите добавить свою валидацию - смело можете создатьValidatorFactoryс фолбэком наDefaultValidatorFactory. Или выпустить новый патч.

Заключение

В общем случае, при проектировании систем, я руководствуюсь правилам SOLID. Про эти принципы говорят не первый десяток лет из каждого утюга, но они все еще актуальны. Местами система выглядит избыточной. Стоит ли заморачиваться насчет сложности и стабильности дизайна кода? Решать вам. Однозначного ответа нет. Определиться вы можете в любой момент. Желательно - на зачаточном этапе. Если вам стало понятно, что ваш проект может жить более чем полгода и он будет расти - пишите гибкий код. Не обманывайте себя, что это все оверинженерия. Мобильных приложений с 2-3 экранами уже давно нет. Разработка под мобильные устройства уже давно вошла в разряд enterprise. Быть маневренным - золотой навык. Ваш бизнес не забуксует на месте и поток запланнированных задач реже станет оставать от графика.

Подробнее..

Представляем бета-версию Jetpack Compose

04.03.2021 14:05:18 | Автор: admin

Совсем недавно, 24 февраля, мы анонсировали запуск бета-версииJetpack Compose. Этот новый набор инструментов для разработки пользовательского интерфейса позволит легко и быстро создавать оригинальные приложения для всех платформ Android. Jetpack Compose предоставляет современные и декларативные API для языка Kotlin для создания привлекательных и быстрых приложений с меньшим объемом кода. Набор совместим с существующими приложениями для Android и библиотеками Jetpack. Кроме того, его можно использовать вместе с Android Views.

Бета-версия Compose это уже готовый API со всеми основными функциями, необходимыми для комфортной работы. Версия стабильная, поэтому мы не будем изменять или удалять API. Финальная версия 1.0 станет доступна уже в этом году. Сейчас самое время начать знакомство с Compose и запланировать применение новых инструментов в следующих проектах и компонентах.

Возможности бета-версии

При создании Compose нашей команде помогали и другие разработчики, которые оставляли свои отзывы. С момента открытия исходного кода в 2019 году мы выпустили 30 публичных версий продукта, получили более 700 внешних отчетов об ошибках и больше 200 внешних дополнений. Нам нравится наблюдать за результатами вашей работы с Сompose, и мы внимательно изучили все отзывы и предложения, чтобы усовершенствовать API и расставить приоритеты при разработке. Мы значительно доработали альфа-версию продукта, а также добавили и улучшили функционал. Вот некоторые из них:

  • Поддержка сопрограмм (новое)

  • Поддержка специальных возможностей для TalkBack. Другие технологии появятся в финальной версии (новое)

  • Новый API для простого использованияанимации(новое)

  • Совместимостьс Views

  • Компоненты Material UIс примерами кода

  • Ленивые списки аналог RecyclerView

  • РазметкаConstraint Layoutна основе DSL

  • Модификаторы

  • Тестирование

  • Темы и графика для простого добавления тёмных и светлых тем

  • Ввод и жесты

  • Редактируемый и обычный текст

  • Управление окнами

Главная задача этой бета-версии обеспечить работу всех API, необходимых в этой и следующих версиях. Мы будем улучшать их стабильность вплоть до финальной версии и уделять внимание производительности и специальным возможностям приложений.

Бета-версия Compose поддерживается в последней версииAndroid Studio Arctic Fox Canary, в которой тоже многоновых инструментов:

  • Live Literals: обновление литералов в реальном времени при предварительном просмотре, на устройстве и в эмуляторе (новое)

  • Предварительный просмотр анимации(новое)

  • Поддержка Compose в инструменте Layout Inspector(новое)

  • Интерактивный предварительный просмотр: воспроизведение сборки, выполненной с помощью Compose, в изолированной среде и взаимодействие с ней (новое)

  • Предварительный просмотр разметки: разметка сборки, выполненной с помощью Compose, прямо на устройстве даже при отсутствии полного приложения (новое)

Live Literals в Android EmulatorLive Literals в Android EmulatorLayout Inspector для Jetpack ComposeLayout Inspector для Jetpack Compose

Работа с уже созданным приложением

Jetpack Compose безупречно работает с Android Views, и вам не придется менять старые привычки. Интерфейсы из Compose можно встроить в Android Views, и наоборот. Вдокументации по совместимостирассказано обо всех возможностях использования этих наборов инструментов.

Compose работает не только с Views, но и с самымираспространенными библиотеками. Вам не придется переписывать приложение. Вот что мы интегрировали:

  • Navigation

  • ViewModel

  • LiveData/RX/Flow

  • Paging

  • Hilt

БиблиотекиMDC-Android Compose Theme AdapterиAccompanistработают с темамиMaterialиAppCompatXML. Вам не придется повторять определения тем. Accompanist также предлагает оболочки распространеннымбиблиотекам для загрузки изображений.

Легкость работы в Compose

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

Jetpack Compose полностью написан на языке Kotlin и использует все егопреимущества, предлагая мощные, простые и интуитивные API. Например,сопрограммыпозволяют создавать простые асинхронные API для описания жестов, анимации и прокрутки. Это облегчает написание кода, сочетающего асинхронные события, например жесты, передающие анимацию, с отменой и очисткой, обеспечиваемой структурированным параллелизмом.

Знакомство с Compose

Чтобы вы ближе познакомились с Jetpack Compose, мы обновили своймини-курсс видео, практическими работами и полезными документами. Сегодня мы также обновилируководство по документации, добавили несколько видеороликов и представили новуюпрактическую работу по созданию анимации. Все это поможет лучше работать с Compose. Помимо статей обархитектуре,специальных возможностяхитестировании, теперь вы можете изучить информацию обанимации,спискахидекларативности.

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

Заключение

В бета-версии Jetpack Compose все нужные API и функции готовы к выходу версии 1.0. Самое время знакомиться с набором инструментов и думать о том, где реализовать его возможности. Мы рады вашимотзывамоб использовании Compose. Своими впечатлениями можно также поделиться с другими разработчиками на канале #compose вKotlin Slack.


Выражаем благодарность за помощь в подготовке статьи коллегам: Анна-Кьяра Беллини(менеджер по продуктам),Ник Бутчер(подразделение по работе с разработчиками) и Звиад Кардава (подразделение по работе с разработчиками)

Подробнее..

Влияние data-классов на вес приложения

04.03.2021 18:06:42 | Автор: admin


Kotlin имеет много классных особенностей: null safety, smart casts, интерполяция строк и другие. Но одной из самых любимых разработчиками, по моим наблюдениям, являются data-классы. Настолько любимой, что их часто используют даже там, где никакой функциональности data-класса не требуется.


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


Data-классы и их функциональность


В процессе разработки часто создаются классы, основное назначение которых хранение данных. В Kotlin их можно пометить как data-классы, чтобы получить дополнительную функциональность:


  • component1(), component2() componentX() для деструктурирующего присваивания (val (name, age) = person);
  • copy() с возможностью создавать копии объекта с изменениями или без;
  • toString() с именем класса и значением всех полей внутри;
  • equals() & hashCode().



Но мы платим далеко не за всю эту функциональность. Для релизных сборок используются оптимизаторы R8, ProGuard, DexGuard и другие. Они могут удалять неиспользуемые методы, а значит, могут и оптимизировать data-классы.


Будут удалены:


  • component1(), component2() componentX() при условии, что не используется деструктурирующее присваивание (но даже если оно есть, то при более агрессивных настройках оптимизации эти методы могут быть заменены на прямое обращение к полю класса);
  • copy(), если он не используется.

Не будут удалены:


  • toString(), поскольку оптимизатор не может знать, будет ли этот метод где-то использоваться или нет (например, при логировании); также он не будет обфусцирован;
  • equals() & hashCode(), потому что удаление этих функций может изменить поведение приложения.

Таким образом, в релизных сборках всегда остаются toString(), equals() и hashCode().


Масштаб изменений


Чтобы понять, какое влияние на размер приложения оказывают data-классы в масштабе приложения, я решил выдвинуть гипотезу: все data-классы в проекте не нужны и могут быть заменены на обычные. А поскольку для релизных сборок мы используем оптимизатор, который может удалять методы componentX() и copy(), то преобразование data-классов в обычные можно свести к следующему:


data class SomeClass(val text: String) {- override fun toString() = ...  - override fun hashCode() = ...- override fun equals() = ...}

Но вручную такое поведение реализовать невозможно. Единственный способ удалить эти функции из кода переопределить их в следующем виде для каждого data-класса в проекте:


data class SomeClass(val text: String) {+ override fun toString() = super.toString()+ override fun hashCode() = super.hashCode()+ override fun equals() = super.equals()}

Вручную для 7749 data-классов в проекте.



Ситуацию усугубляет использование монорепозитория для приложений. Это означает, что я не знаю точно, сколько из этих 7749 классов мне нужно изменить, чтобы измерить влияние data-классов только на одно приложение. Поэтому придётся менять все!


Плагин компилятора


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


В открытом доступе на GitHub есть плагин Sekret, который позволяет скрывать в toString() указанные аннотацией поля в data-классах. Его я и взял за основу своего плагина.


С точки зрения создания структуры проекта практически ничего не поменялось. Нам понадобятся:


  • Gradle-плагин для простой интеграции;
  • плагин компилятора, который будет подключён через Gradle-плагин;
  • проект с примером, на котором можно запускать различные тесты.

Самая важная часть в Gradle-плагине это объявление KotlinGradleSubplugin. Этот сабплагин будет подключён через ServiceLocator. С помощью основного Gradle-плагина мы можем конфигурировать KotlinGradleSubplugin, который будет настраивать поведение плагина компилятора.


@AutoService(KotlinGradleSubplugin::class)class DataClassNoStringGradleSubplugin : KotlinGradleSubplugin<AbstractCompile> {    // Проверяем, есть ли основной Gradle-плагин    override fun isApplicable(project: Project, task: AbstractCompile): Boolean =        project.plugins.hasPlugin(DataClassNoStringPlugin::class.java)    override fun apply(        project: Project,        kotlinCompile: AbstractCompile,        javaCompile: AbstractCompile?,        variantData: Any?,        androidProjectHandler: Any?,        kotlinCompilation: KotlinCompilation<KotlinCommonOptions>?    ): List<SubpluginOption> {        // Опции плагина компилятора настраиваются через DataClassNoStringExtension с помощью Gradle build script        val extension =            project                .extensions                .findByType(DataClassNoStringExtension::class.java)                ?: DataClassNoStringExtension()        val enabled = SubpluginOption("enabled", extension.enabled.toString())        return listOf(enabled)    }    override fun getCompilerPluginId(): String = "data-class-no-string"    // Это артефакт плагина компилятора, и он должен быть доступен в репозитории Maven, который вы используете    override fun getPluginArtifact(): SubpluginArtifact =        SubpluginArtifact("com.cherryperry.nostrings", "kotlin-plugin", "1.0.0")}

Плагин компилятора состоит из двух важных компонентов: ComponentRegistrar и CommandLineProcessor. Первый отвечает за интеграцию нашей логики в этапы компиляции, а второй за обработку параметров нашего плагина. Я не буду описывать их детально посмотреть реализацию можно в репозитории. Отмечу лишь, что, в отличие от метода, описанного в другой статье, мы будем регистрировать ClassBuilderInterceptorExtension, а не ExpressionCodegenExtension.


ClassBuilderInterceptorExtension.registerExtension(    project = project,    extension = DataClassNoStringClassGenerationInterceptor())

ClassBuilderInterceptorExtension позволяет изменять процесс генерации классов, а значит, с его помощью мы сможем избежать создания ненужных методов.


class DataClassNoStringClassGenerationInterceptor : ClassBuilderInterceptorExtension {    override fun interceptClassBuilderFactory(        interceptedFactory: ClassBuilderFactory,        bindingContext: BindingContext,        diagnostics: DiagnosticSink    ): ClassBuilderFactory =        object : ClassBuilderFactory {            override fun newClassBuilder(origin: JvmDeclarationOrigin): ClassBuilder {                val classDescription = origin.descriptor as? ClassDescriptor                // Если класс является data-классом, то изменяем процесс генерации кода                return if (classDescription?.kind == ClassKind.CLASS && classDescription.isData) {                    DataClassNoStringClassBuilder(interceptedFactory.newClassBuilder(origin), removeAll)                } else {                    interceptedFactory.newClassBuilder(origin)                }            }        }}

Теперь необходимо не дать компилятору создать некоторые методы. Для этого воспользуемся DelegatingClassBuilder. Он будет делегировать все вызовы оригинальному ClassBuilder, но при этом мы сможем переопределить поведение метода newMethod. Если мы попытаемся создать методы toString(), equals(), hashCode(), то вернём пустой MethodVisitor. Компилятор будет писать в него код этих методов, но он не попадёт в создаваемый класс.


class DataClassNoStringClassBuilder(    val classBuilder: ClassBuilder) : DelegatingClassBuilder() {    override fun getDelegate(): ClassBuilder = classBuilder    override fun newMethod(        origin: JvmDeclarationOrigin,        access: Int,        name: String,        desc: String,        signature: String?,        exceptions: Array<out String>?    ): MethodVisitor {        return when (name) {            "toString",            "hashCode",            "equals" -> EmptyVisitor            else -> super.newMethod(origin, access, name, desc, signature, exceptions)        }    }    private object EmptyVisitor : MethodVisitor(Opcodes.ASM5)}

Таким образом, мы вмешались в процесс создания data-классов и полностью исключили из них вышеуказанные методы. Убедиться, что этих методов больше нет, можно с помощью кода, доступного в sample-проекте. Также можно проверить JAR/DEX-байт-код и убедиться в том, что там эти методы отсутствуют.


class AppTest {    data class Sample(val text: String)    @Test    fun `toString method should return default string`() {        val sample = Sample("test")        // toString должен возвращать результат метода Object.toString        assertEquals(            "${sample.javaClass.name}@${Integer.toHexString(System.identityHashCode(sample))}",            sample.toString()        )    }    @Test    fun `hashCode method should return identityHashCode`() {         // hashCode должен возвращать результат метода Object.hashCode, он же по умолчанию System.identityHashCode        val sample = Sample("test")        assertEquals(System.identityHashCode(sample), sample.hashCode())    }    @Test    fun `equals method should return true only for itself`() {        // equals должен работать как Object.equals, а значит, должен быть равным только самому себе        val sample = Sample("test")        assertEquals(sample, sample)        assertNotEquals(Sample("test"), sample)    }}

Весь код доступен в репозитории, там же есть пример интеграции плагина.


Результаты



Для сравнения мы будем использовать релизные сборки Bumble и Badoo. Результаты были получены с помощью утилиты Diffuse, которая выводит детальную информацию о разнице между двумя APK-файлами: размеры DEX-файлов и ресурсов, количество строк, методов, классов в DEX-файле.


Приложение Bumble Bumble (после) Разница Badoo Badoo (после) Разница
Data-классы 4026 - - 2894 - -
Размер DEX (zipped) 12.4 MiB 11.9 MiB -510.1 KiB 15.3 MiB 14.9 MiB -454.1 KiB
Размер DEX (unzipped) 31.7 MiB 30 MiB -1.6 MiB 38.9 MiB 37.6 MiB -1.4 MiB
Строки в DEX 188969 179197 -9772 244116 232114 -12002
Методы 292465 277475 -14990 354218 341779 -12439


Количество data-классов было определено эвристическим путём с помощью анализа удалённых из DEX-файла строк.



Реализация toString() у data-классов всегда начинается с короткого имени класса, открывающей скобки и первого поля data-класса. Data-классов без полей не существует.


Исходя из результатов, можно сказать, что в среднем каждый data-класс обходится в 120 байт в сжатом и 400 байт в несжатом виде. На первый взгляд, не много, поэтому я решил проверить, сколько получается в масштабе целого приложения. Выяснилось, что все data-классы в проекте обходятся нам в ~4% размера DEX-файла.


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


Использование data-классов


Я ни в коем случае не призываю вас отказываться от data-классов, но, принимая решение об их использовании, нужно тщательно всё взвесить. Вот несколько вопросов, которые стоит задать себе перед объявлением data-класса:


  • Нужны ли реализации equals() и hashCode()?
    • Если нужны, лучше использовать data-класс, но помните про toString(), он не обфусцируется.
  • Нужно ли использовать деструктурирующее присваивание?
    • Использовать data-классы только ради этого не лучшее решение.
  • Нужна ли реализация toString()?
    • Вряд ли существует бизнес-логика, зависящая от реализации toString(), поэтому иногда можно генерировать этот метод вручную, средствами IDE.
  • Нужен ли простой DTO для передачи данных в другой слой или задания конфигурации?
    • Обычный класс подойдёт для этих целей, если не требуются предыдущие пункты.

Мы не можем совсем отказаться от использования data-классов в проекте, и приведённый выше плагин ломает работоспособность приложения. Удаление методов было сделано ради оценки влияния большого количества data-классов. В нашем случае это ~4% от размера DEX-файла приложения.


Если вы хотите оценить, сколько места занимают data-классы в вашем приложении, то можете сделать это самостоятельно с помощью моего плагина.

Подробнее..

Android ViewPager2 заменяем фрагменты на лету (программно)

05.03.2021 16:08:13 | Автор: admin

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

Дано

  • ViewPager2

  • список Fragment-ов (например, разделы анкеты или многостраничный раздел настроек)

  • Kotlin (Java), Android собственно

Задача

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

Пример:

Есть три фрагмента. Первый - список любимых тарелок, второй - экран с сообщениями, третий - настройки.

Пользователь на первом экране выбирает тарелку -> меняем первый фрагмент на фрагмент "Информация о тарелке"

Пользователь листает свайпами или выбирает через TabLayout третий экран - "Настройки", выбирает раздел "Выбор языка" -> меняем третий фрагмент на фрагмент "Выбор языка"

Теперь пользователь, не выходя из выбора языка, листает до первого фрагмента. Он должен увидеть информацию о тарелке, а не список любимых тарелок.

Итак, пытаемся сдружить ArrayMap и ViewPager2.

Результат

Будет такой

Дисклеймер (не читать, вода)

Пишу, потому что найти хороших решений по этой задаче не смог, хотя вроде пытался усердно.

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

Весь проект - https://github.com/IDolgopolov/ViewPager2_FragmentReplacer

Иногда буду использовать слово "страницы" - имею ввиду позицию, на которой отображается фрагмент во ViewPager. Например, "Список любимых тарелок", "Экран сообщений" и "Настройки" - три страницы. Фрагмент "Выбор языка" заменяет третью страницу.

Решение

Сначала ничего интересного, просто для общего ознакомления

Верстка

main_activity.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical"    >    <com.google.android.material.tabs.TabLayout        android:id="@+id/tab_layout"        android:layout_width="match_parent"        android:layout_height="wrap_content" />    <androidx.viewpager2.widget.ViewPager2        android:id="@+id/view_pager_2"        android:layout_width="match_parent"        android:layout_height="match_parent"        tools:context=".MainActivity"/></LinearLayout>
fragment_one.xml
<?xml version="1.0" encoding="utf-8"?><RelativeLayout    xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <Button        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Замени меня"        android:id="@+id/b_replace_fragment_one"        android:layout_centerInParent="true"        android:layout_marginHorizontal="20dp"/></RelativeLayout>
fragment_deep.xml
<?xml version="1.0" encoding="utf-8"?><RelativeLayout    xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <Button        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="Верни назад, блин"        android:id="@+id/b_back"        android:layout_margin="20dp"        /></RelativeLayout>
fragment_two.xml, fragment_three.xml
<?xml version="1.0" encoding="utf-8"?><TextView    android:layout_width="match_parent"    android:layout_height="match_parent"    android:text="Фрагмент 3"    android:gravity="center"    xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android" />

Интерфейсы

Опишем функции, которые нам понадобятся для смены фрагментов

interface FragmentReplacer {    fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean = true)    fun replaceDef(position: Int, isNotify: Boolean = true) : BaseFragment}

Все фрагменты, добавляемые во ViewPagerAdapter (MyViewPager2Adapter описан ниже), будут наследоваться от BaseFragment.

Определим в нем переменные для хранения:

  • pageId - уникальный номер страницы. Может быть любым числом, но главное - уникальным и большим, чем у вас позиций страниц (pageId > PAGE_COUNT - 1), иначе будут баги из-за метода getItemId()

  • pagePos - номер странице, на которой будет отображаться фрагмент во ViewPager (начиная с 0, естественно)

  • fragmentReplacer - ссылка на ViewPagerAdapter (MyViewPager2Adapter реализует FragmentReplacer)

abstract class BaseFragment(private val layoutId: Int) : Fragment() {    val pageId = Random.nextLong(2021, 2021*3)    var pagePos = -1    protected lateinit var fragmentReplacer: FragmentReplacer    override fun onCreateView(        inflater: LayoutInflater,        container: ViewGroup?,        savedInstanceState: Bundle?    ): View? {        return inflater.inflate(layoutId, container, false)    }    fun setPageInfo(pagePos: Int, fragmentReplacer: FragmentReplacer) {        this.pagePos = pagePos        this.fragmentReplacer = fragmentReplacer    }}

Связь активити и ViewPager2

class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        //MyViewPager2Adapter - смотреть разбор далее        view_pager_2.adapter = MyViewPager2Adapter(this)        TabLayoutMediator(tab_layout, view_pager_2) { tab, position ->            tab.text = when(position) {                0 -> "Первый"                1 -> "Второй"                2 -> "Крайний"                else -> throw IllegalStateException()            }        }.attach()    }}

Адаптер для ViewPager2 - всё здесь

В этом классе будут реализован интерфейс FragmentReplacer и переопределены несколько классов FragmentStateAdapter. Это и позволит менять фрагменты на лету.

Весь код класса, который ниже разбираю подробно
class MyViewPager2Adapter(container: FragmentActivity) : FragmentStateAdapter(container),    FragmentReplacer {        companion object {        private const val PAGE_COUNT = 3    }    private val mapOfFragment = ArrayMap<Int, BaseFragment>()    override fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean) {        mapOfFragment[position] = newFragment        if (isNotify)            notifyItemChanged(position)    }    override fun replaceDef(position: Int, isNotify: Boolean): BaseFragment {        val fragment = when (position) {            0 -> FragmentOne()            1 -> FragmentTwo()            2 -> FragmentThree()            else -> throw IllegalStateException()        }        fragment.setPageInfo(            pagePos = position,            fragmentReplacer = this        )        replace(position, fragment, isNotify)        return fragment    }    override fun createFragment(position: Int): Fragment {        return mapOfFragment[position] ?: replaceDef(position, false)    }    override fun containsItem(itemId: Long): Boolean {        var isContains = false        mapOfFragment.values.forEach {            if (it.pageId == itemId) {                isContains = true                return@forEach            }        }        return isContains    }    override fun getItemId(position: Int) =        mapOfFragment[position]?.pageId ?: super.getItemId(position)    override fun getItemCount() = PAGE_COUNT}

В первую очередь переопределим четыре метода FragmentStateAdapter:

//ключ - позиция страницы//значения - фрагмент, отображаемый в данный момент на этой страницеprivate val mapOfFragment = ArrayMap<Int, BaseFragment>()//возвращаем количество страниц (в нашем примере - 3)override fun getItemCount() = PAGE_COUNT/*эта функция вызывается, когда пользователь впервые открывает страницуили был вызван notifyItemChanged(position)    если фрагмент еще не был создан (mapOfFragment[position] == null),  то заменяем фрагмент на этой странице фрагментов по умолчанию*/override fun createFragment(position: Int): Fragment {return mapOfFragment[position] ?: replaceDef(position, false)}/*у каждого нашего фрагмента есть свой id, отдаем его адаптеруесли фрагмент на этой позиции еще не был создан, можно вернуть любую чипуху, не совпадающую со всеми существующими id  p.s. super.getItemId { return position } по умолчанию*/override fun getItemId(position: Int) =mapOfFragment[position]?.pageId ?: super.getItemId(position)override fun containsItem(itemId: Long): Boolean {    var isContains = false       mapOfFragment.values.forEach { if (it.pageId == itemId) {           isContains = true           return@forEach       } }       return isContains}

Теперь две функции, которые позволяют подменять фрагменты

override fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean) {    //указываем фрагменту, на какой странице он отображается    newFragment.setPageInfo(            pagePos = position,            fragmentReplacer = this        )        mapOfFragment[position] = newFragment  //isNotify = true всегда, кроме вызова replace() из функции createFragment()  //иначе приложение будет крашится, так как еще ничего не отображено    if (isNotify)    notifyItemChanged(position)}//здесь указываем базовые фрагменты - фрагменты, отображаемые по умолчанию//пригодится, когда захотим отобразить страницы в первый раз//или вернуться к дефолтной странице после того, как закончим все дела на замененнойoverride fun replaceDef(position: Int, isNotify: Boolean): BaseFragment {    val fragment = when (position) {            0 -> FragmentOne()            1 -> FragmentTwo()            2 -> FragmentThree()            else -> throw IllegalStateException()    }    replace(position, fragment, isNotify)    return fragment}

Пример использования

Все готово - у каждого фрагмента есть ссылка на адаптер, значит есть возможность заменить фрагмент на любой странице, а самое главное (наиболее используемое на практике) - есть возмжность заменить самого себя.

Например, фрагмент, по умолчанию, отображаемый на первой странице:

FragmentOne.kt

class FragmentOne : BaseFragment(R.layout.fragment_one) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        //по нажатию кнопки        b_replace_fragment_one.setOnClickListener {          //создаем фрагмент, который заменит этот фрагмент            val fragmentDeep = FragmentDeep()          //заменяем             fragmentReplacer.replace(this.pagePos, fragmentDeep)        }    }}

FragmentDeep.kt

class FragmentDeep : BaseFragment(R.layout.fragment_deep) {override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        b_back.setOnClickListener {          //по нажатию кнопки заменяем текущий фрагмент           //на дефолтный: 0 -> FragmentOne()            fragmentReplacer.replaceCurrentToDef()                        //или            //fragmentReplacer.replace(pagePos, FragmentOne())                        //или            //fragmentReplacer.replace(0, FragmentOne())        }    }}

Мысли в слух

Аккуратно, если фрагменты тяжелые, - надо задуматься об очищении mapOfFragment.

Возможно, стоит хранить Class<BaseFragment> вместо BaseFragment. Но тогда придется инициализировать их каждый раз в createFragment(). Меньше памяти, больше времени. Что думаете?

На правах...

dendolg.ru - портфолио, контакты для сотрудничества (после переезда на GithubPages браузер иногда банит https протокол, а иногда не банит - разбираюсь)

t.me/dolgopolovd - блог с нулем подписчиков, в котором рассуждаю о мобильном и промышленном дизайне и иногда отвлекаюсь от программирования

Подробнее..

Анонс вебинара Создаём мультиплатформенное Flutter приложение в интерфейсе Cyberpunk 2077

24.02.2021 20:16:16 | Автор: admin

На живом примере приложения в визуальном стиле игры Cyberpunk 2077 покажем всю мощь фреймворка Flutter и возможность сборки единого кода под разные платформы.

Вместе с вами в прямом эфире соберём по-настоящему мультиплатформенное приложение для веба, iOS, Android и desktop.

РЕГИСТРАЦИЯ

Что будет

  • Разработаем вместе аутентичный дизайн, кастомные виджеты и опубликуем.

  • В UI ките игры Cyberpunk 2077 рисуем кнопку, панельку, градиентный фон, скролл, вращающийся логотип. Билдим.

  • Адаптируем верстку для разных экранов и платформ.

  • Реализуем навигацию между экранами.

  • Открытый исходный код.

Примеры нескольких UI элементов, которые будем создавать в ходе вебинараПримеры нескольких UI элементов, которые будем создавать в ходе вебинара

Вебинар проводят разработчики компании Surf

Андрей Савостьянов, Flutter-разработчик Surf. В прошлом Андрей работал с Java/Spring/Android создавал железные решения и протоколы, прикладные системы по автоматизации и мониторингу технологических комплексов. Почти 2 года назад сменил специализацию и ни разу не пожалел. Андрей большой энтузиаст dart/flutter и даже делает fullstack приложения на дарте.

Михаил Зотьев, Flutter-разработчик Surf.Flutter-разработчик, тимлид. Активный спикер Surf, попал ТОП-3 всех докладчиков на Mobius 2020 и DartUp 2020. Ещё Михаил ведёт телеграм-канал о Flutter-разработке Oh, my Flutter

Регистрация

Вебинар начнётся 25 февраля в 18:00 МСК на YouTube-канале Surf.

Чтобы поучаствовать в нём, заполните форму регистрации. Мы отправим напоминание и ссылку на трансляцию за день и за час до начала вебинара.

До встречи!

РЕГИСТРАЦИЯ

Подробнее..

Запускаем Rust-приложение на мобильной ОС Аврора

02.03.2021 10:09:04 | Автор: admin

Всем привет! Меня зовут Шамиль, я ведущий инженер-разработчик в КРОК. Помимо всего прочего мы в компании занимаемся ещё и разработкой мобильных приложений для операционной системы Аврора, есть даже центр компетенций по ней.

Для промышленной разработки мы, конечно же, пока используем связку C++ и QML, но однажды подсев на "ржавую" иглу Rust, я не мог не попробовать применить свой любимый язык программирования для написания мобильных приложений. В этой статье я опишу эксперимент по написанию простейшего приложения на Rust, предназначенного для запуска на мобильном устройстве под управлением вышеупомянутой ОС. Сразу оговорюсь, что легких путей я не искал эксперименты проводил на сертифицированной версии Авроры, которая добавила огонька в этот процесс. Но, как говорится, только защищённая ОС, только хардкор.

Пара выходных у меня ушла только на то, чтобы запустить минимальное консольное приложение (речь о нём пойдёт в первой части), ещё пара дней на эксперименты с графическим интерфейсом, выбор оптимального подхода и запуск приложения с GUI (этому посвящена вторая часть повествования). В итоге получился минимальный скелет мобильного приложения, готового к сборке и запуску, на который при желании уже можно наращивать мясо.

Готовим окружение

Итак, работа будет вестись из-под Ubuntu Linux с уже установленным Rust. В качестве подопытного планшета выступает Aquarius NS220 с сертифицированной ОС Аврора последней (на момент написания статьи) версии 3.2.2 с включённым режимом разработчика, который обеспечивает связь по SSH, а также привилегированный доступ с правами суперпользователя.

Первым делом добавим средства кросскомпилятора для архитектуры ARM, так как на целевом планшете стоит именно такой процессор.

sudo apt install -y g++-arm-linux-gnueabihfrustup target add armv7-unknown-linux-gnueabihf

В сертифицированной версии ОС Аврора не разрешается запускать неподписанные приложения. Подписывать надо проприетарной утилитой из состава Aurora Certified SDK под названием ompcert-cli, которая поддерживает на входе только пакет в формате RPM. Поэтому сразу установим замечательную утилиту cargo-rpm, которая возьмёт на себя всю рутинную работу по упаковке приложения в RPM-пакет:

cargo install cargo-rpm

Саму процедуру подписывания RPM-пакета я описывать не буду, она неплохо документирована в справочных материалах ОС Аврора.

Aurora SDK можно скачать с сайта производителя.

Часть 1. Hello. World

TL;DR Исходники проекта можно найти в репозитории на Гитхабе.

Создаем минимальный проект

Создаём пустое приложение на Rust:

cargo new aurora-rust-helloworld

Пытаемся сгенерировать .spec файл для RPM-пакета:

cargo rpm init

Получаем ошибки, что не хватает некоторых полей в Cargo.toml, добавляем их:

Cargo.toml:

[package]name = "aurora-rust-helloworld"version = "0.1.0"authors = ["Shamil Yakupov <syakupov@croc.ru>"]edition = "2018"description = "Rust example for Aurora OS"license = "MIT"

Закидываем в папку .cargo конфигурационный файл с указанием правильного линкера для компоновки исполняемого файла под архитектуру ARM:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]linker = "arm-linux-gnueabihf-gcc"

Собираем RPM-пакет:

cargo rpm initcargo rpm build -v --target=armv7-unknown-linux-gnueabihf

Всё собралось, забираем RPM из папки target/armv7-unknown-linux-gnueabihf/release/rpmbuild/RPMS/armv7hl, подписываем его, копируем на планшет и пытаемся установить:

$ devel-suPassword:# pkcon install-local ./aurora-rust-helloworld-0.1.0-1.armv7hl.rpm

Получаем ошибку:

Fatal error: nothing provides libc.so.6(GLIBC_2.32) needed by aurora-rust-helloworld-0.1.0-1.armv7hl

Смотрим версию glibc на устройстве, и понимаем, что она явно ниже той, что нам требуется:

$ ldd --versionldd (GNU libc) 2.28

Что ж, тогда попробуем забрать нужные библиотеки с планшета, закинуть их в директорию lib и слинковать с ними. Для верности будем пользоваться линкером, входящим в состав Aurora SDK, который закинем в директорию bin. Для начала посмотрим, какие именно библиотеки нам нужны. Меняем содержимое .cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Пробуем собрать:

cargo build --release --target=armv7-unknown-linux-gnueabihf

Получаем ошибки:

aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lgcc_saurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lutilaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lrtaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lpthreadaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lmaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -ldlaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lcaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lutil

Копируем недостающие библиотеки с планшета:

mkdir -p libscp nemo@192.168.2.15:/usr/lib/libgcc_s.so ./libscp nemo@192.168.2.15:/usr/lib/libutil.so ./libscp nemo@192.168.2.15:/usr/lib/librt.so ./libscp nemo@192.168.2.15:/usr/lib/libpthread.so ./libscp nemo@192.168.2.15:/usr/lib/libm.so ./libscp nemo@192.168.2.15:/usr/lib/libdl.so ./libscp nemo@192.168.2.15:/usr/lib/libc.so ./libscp nemo@192.168.2.15:/usr/lib/libutil.so ./lib

Снова пытаемся собрать, получаем новую порцию ошибок:

aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: skipping incompatible /lib/libc.so.6 when searching for /lib/libc.so.6aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /lib/libc.so.6aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: skipping incompatible /usr/lib/libc_nonshared.a when searching for /usr/lib/libc_nonshared.aaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /usr/lib/libc_nonshared.aaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /lib/ld-linux-armhf.so.3

Копируем недостающее:

scp nemo@192.168.2.15:/lib/libc.so.6 ./libscp nemo@192.168.2.15:/usr/lib/libc_nonshared.a ./libscp nemo@192.168.2.15:/lib/ld-linux-armhf.so.3 ./lib

Ещё надо подредактировать файл libc.so (который является фактически скриптом линкера), чтобы дать понять линкеру, где надо искать библиотеки:

lib/libc.so:

/* GNU ld script   Use the shared library, but some functions are only in   the static library, so try that secondarily.  */OUTPUT_FORMAT(elf32-littlearm)GROUP ( libc.so.6 libc_nonshared.a  AS_NEEDED ( ld-linux-armhf.so.3 ) )

Запускаем сборку RPM-пакета, копируем, пытаемся установить.

Здесь позволю себе небольшое лирическое отступление. Перед установкой RPM-пакета на сертифицированной версии ОС Аврора запускается RPM-валидатор утилита, которая проверяет, насколько собранный пакет удовлетворяет требованиям системы. И до тех пор, пока пакет не пройдёт валидацию, установить приложение не получится. Безопасность превыше всего.

Итак, мы видим, что валидатор выдал несколько ошибок:

вот таких
Desktop file============ERROR [/usr/share/applications/aurora-rust-helloworld.desktop] File is missing - cannot validate .desktop filePaths=====WARNING [/usr/share/aurora-rust-helloworld] Directory not foundERROR [/usr/share/applications/aurora-rust-helloworld.desktop] File not foundWARNING [/usr/share/icons/hicolor/86x86/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/108x108/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/128x128/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/172x172/apps/aurora-rust-helloworld.png] File not foundERROR [/usr/share/icons/hicolor/[0-9x]{5,9}/apps/aurora-rust-helloworld.png] No icons found! RPM must contain at least one icon, see: https://community.omprussia.ru/doc/software_development/guidelines/rpm_requirementsLibraries=========ERROR [/usr/bin/aurora-rust-helloworld] Cannot link to shared library: libutil.so.1Symbols=======ERROR [/usr/bin/aurora-rust-helloworld] Binary does not link to 9__libc_start_main@GLIBC_2.4.Requires========ERROR [libutil.so.1] Cannot require shared library: 'libutil.so.1'

Что ж, будем бороться с каждой ошибкой по списку.

Добавляем недостающие файлы

Добавим иконки и ярлык (файл с расширением desktop) в директорию .rpm.

.rpm/aurora-rust-helloworld.desktop:

[Desktop Entry]Type=ApplicationX-Nemo-Application-Type=silica-qt5Icon=aurora-rust-helloworldExec=aurora-rust-helloworldName=Rust Hello-World

Для того, чтобы копировать нужные файлы на этапе сборки RPM-пакета, сделаем простенький Makefile:

Makefile
.PHONY: all clean install prepare release rpmall:@cargo build --target=armv7-unknown-linux-gnueabihfclean:@rm -rvf targetinstall:@scp ./target/armv7-unknown-linux-gnueabihf/release/aurora-rust-helloworld nemo@192.168.2.15:/home/nemo/@scp ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/RPMS/armv7hl/*.rpm nemo@192.168.2.15:/home/nemo/prepare:@rustup target add armv7-unknown-linux-gnueabihf@cargo install cargo-rpmrelease:@cargo build --release --target=armv7-unknown-linux-gnueabihfrpm:@mkdir -p ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cp -vf .rpm/aurora-rust-helloworld.desktop ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cp -rvf .rpm/icons ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cargo rpm build -v --target=armv7-unknown-linux-gnueabihf

Обновим aurora-rust-helloworld.spec:

.rpm/aurora-rust-helloworld.spec
%define __spec_install_post %{nil}%define __os_install_post %{_dbpath}/brp-compress%define debug_package %{nil}Name: aurora-rust-helloworldSummary: Rust example for Aurora OSVersion: @@VERSION@@Release: @@RELEASE@@%{?dist}License: MITGroup: Applications/SystemSource0: %{name}-%{version}.tar.gzSource1: %{name}.desktopSource2: iconsBuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root%description%{summary}%prep%setup -q%installrm -rf %{buildroot}mkdir -p %{buildroot}cp -a * %{buildroot}mkdir -p %{buildroot}%{_datadir}/applicationscp -a %{SOURCE1} %{buildroot}%{_datadir}/applicationsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/86x86/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/108x108/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/128x128/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/172x172/appscp -a %{SOURCE2}/86x86/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/86x86/appscp -a %{SOURCE2}/108x108/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/108x108/appscp -a %{SOURCE2}/128x128/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/128x128/appscp -a %{SOURCE2}/172x172/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/172x172/apps%cleanrm -rf %{buildroot}%files%defattr(-,root,root,-)%{_bindir}/*%{_datadir}/applications/%{name}.desktop%{_datadir}/icons/hicolor/*/apps/%{name}.png

Для сборки пакета теперь достаточно выполнить:

make rpm

Убираем зависимость от libutil.so

Я не нашёл способа, как убедить cargo при вызове команды линкера не передавать ключ -lutil. Вместо этого я решил создать скрипт-заглушку для линкера с какой-нибудь незначащей командой.

lib/libutil.so:

/* GNU ld script   Dummy script to avoid dependency on libutil.so */ASSERT(1, "Unreachable")

Да, не самый хороший способ. Если кто-нибудь знает, как сделать лучше, делитесь в комментариях.

Добавляем символ __libc_start_main

Перепробовав несколько способов, остановился на том, чтобы добавить при линковке стандартный объектный файл crt1.o. Копируем его с планшета:

scp nemo@192.168.2.15:/usr/lib/crt1.o ./lib

И добавляем в команды линкера:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Однако при попытке сборки получаем ошибки:

undefined reference to `__libc_csu_fini'undefined reference to `__libc_csu_init'

Добавим заглушки этих функций в main.rs:

src/main.rs:

#[no_mangle]pub extern "C" fn __libc_csu_init() {}#[no_mangle]pub extern "C" fn __libc_csu_fini() {}fn main() {    println!("Hello, world!");}

Ещё один быстрый и грязный хак, зато теперь RPM-пакет проходит валидацию и устанавливается!

Момент истины близок, запускаем на планшете и получаем очередную ошибку:

$ aurora-rust-helloworld-bash: /usr/bin/aurora-rust-helloworld: /usr/lib/ld.so.1: bad ELF interpreter: No such file or directory

Смотрим зависимости:

$ ldd /usr/bin/aurora-rust-helloworldlinux-vdso.so.1 (0xbeff4000)libgcc_s.so.1 => /lib/libgcc_s.so.1 (0xa707f000)librt.so.1 => /lib/librt.so.1 (0xa7069000)libpthread.so.0 => /lib/libpthread.so.0 (0xa7042000)libm.so.6 => /lib/libm.so.6 (0xa6fc6000)libdl.so.2 => /lib/libdl.so.2 (0xa6fb3000)libc.so.6 => /lib/libc.so.6 (0xa6e95000)/usr/lib/ld.so.1 => /lib/ld-linux-armhf.so.3 (0xa70e7000)

И видим динамическую линковку с библиотекой ld-linux-armhf.so.3. Если решать в лоб, то нужно создать символическую ссылку /usr/lib/ld.so.1 /lib/ld-linux-armhf.so.3 (и это даже будет неплохо работать). Но, к сожалению, такое решение не подходит. Дело в том, что строгий RPM-валидатор не пропустит ни пред(пост)-установочные скрипты в .spec-файле, ни деплой в директорию /usr/lib. Вообще список того, что можно, приведён здесь.

Долгое и разнообразное гугление подсказало, что у линкера GCC есть нужный нам ключ (dynamic-linker), который позволяет сослаться непосредственно на нужную зависимость. Правим config.toml:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o --dynamic-linker /lib/ld-linux-armhf.so.3"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Собираем RPM-пакет, подписываем, копируем на планшет, устанавливаем и с замиранием сердца запускаем:

$ aurora-rust-helloworldHello, world!

Часть 2. Запускаем приложение с GUI

TL;DR Исходники проекта можно найти в репозитории.

В Авроре всё очень сильно завязано на Qt/QML, поэтому сначала я думал использовать крейт qmetaobject. Однако в комплекте с ОС идёт библиотека Qt версии 5.6.3, а qmetaobject, судя по описанию, требует минимум Qt 5.8. И действительно, попытка сборки крейта приводит к ошибкам.

Поэтому я пошёл по пути точечных заимствований из исходников qmetaobject благо, лицензия позволяет.

Для начала копируем проект, созданный в предыдущей части, и переименовываем его в aurora-rust-gui.

Приступаем

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

вот таких
scp nemo@192.168.2.15:/usr/lib/libstdc++.so ./libscp nemo@192.168.2.15:/usr/lib/libQt5Core.so.5 ./lib/libQt5Core.soscp nemo@192.168.2.15:/usr/lib/libQt5Gui.so.5 ./lib/libQt5Gui.soscp nemo@192.168.2.15:/usr/lib/libQt5Qml.so.5 ./lib/libQt5Qml.soscp nemo@192.168.2.15:/usr/lib/libQt5Quick.so.5 ./lib/libQt5Quick.soscp nemo@192.168.2.15:/usr/lib/libGLESv2.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libpng16.so.16 ./libscp nemo@192.168.2.15:/usr/lib/libz.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libicui18n.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libicuuc.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libpcre16.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libglib-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libsystemd.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libQt5Network.so.5 ./libscp nemo@192.168.2.15:/lib/libresolv.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libhybris-common.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libicudata.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libpcre.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libselinux.so.1 ./libscp nemo@192.168.2.15:/usr/lib/liblzma.so.5 ./libscp nemo@192.168.2.15:/usr/lib/libgcrypt.so.11 ./libscp nemo@192.168.2.15:/usr/lib/libgpg-error.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libcap.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libsailfishapp.so.1 ./lib/libsailfishapp.soscp nemo@192.168.2.15:/usr/lib/libmdeclarativecache5.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libmlite5.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libdconf.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libgobject-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libQt5DBus.so.5 ./libscp nemo@192.168.2.15:/usr/lib/libdconf.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libffi.so.6 ./libscp nemo@192.168.2.15:/usr/lib/libdbus-1.so.3 ./libscp nemo@192.168.2.15:/usr/lib/libgio-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libgmodule-2.0.so.0 ./lib

А еще копируем заголовочные файлы, которые идут в составе Aurora SDK:

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/qt5 include/qt5

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/sailfishapp include/sailfishapp

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/GLES3 include/GLES3

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/KHR include/KHR

Для сборки проекта напишем скрипт build.rs и укажем его в Cargo.toml.

build.rs:

fn main() {    let include_path = "include";    let qt_include_path = "include/qt5";    let sailfish_include_path = "include/sailfishapp";    let library_path = "lib";    let mut config = cpp_build::Config::new();    config        .include(include_path)        .include(qt_include_path)        .include(sailfish_include_path)        .opt_level(2)        .flag("-std=gnu++1y")        .flag("-mfloat-abi=hard")        .flag("-mfpu=neon")        .flag("-mthumb")        .build("src/main.rs");    println!("cargo:rustc-link-search={}", library_path);    println!("cargo:rustc-link-lib=sailfishapp");    println!("cargo:rustc-link-lib=Qt5Gui");    println!("cargo:rustc-link-lib=Qt5Core");    println!("cargo:rustc-link-lib=Qt5Quick");    println!("cargo:rustc-link-lib=Qt5Qml");}

Cargo.toml:

[package]# ...build = "build.rs"[dependencies]cpp = "0.5.6"[build-dependencies]cpp_build = "0.5.6"#...

Теперь возьмёмся за само приложение. За создание инстанса приложения у нас будет отвечать структура SailfishApp по аналогии с приложением для Авроры, написанном на C++.

src/main.rs:

#[macro_use]extern crate cpp;mod qbytearray;mod qstring;mod qurl;mod sailfishapp;use sailfishapp::SailfishApp;#[no_mangle]pub extern "C" fn __libc_csu_init() {}#[no_mangle]pub extern "C" fn __libc_csu_fini() {}fn main() {    let mut app = SailfishApp::new();    app.set_source("main.qml".into());    app.show();    app.exec();}

SailfishApp это по сути обвязка (биндинги) к соответствующему классу на C++. Берём за образец структуру QmlEngine из крейта qmetaobject.

src/sailfishapp.rs
use crate::qstring::QString;cpp! {{    #include <sailfishapp.h>    #include <QtCore/QDebug>    #include <QtGui/QGuiApplication>    #include <QtQuick/QQuickView>    #include <QtQml/QQmlEngine>    #include <memory>    struct SailfishAppHolder {        std::unique_ptr<QGuiApplication> app;        std::unique_ptr<QQuickView> view;        SailfishAppHolder() {            qDebug() << "SailfishAppHolder::SailfishAppHolder()";            int argc = 1;            char *argv[] = { "aurora-rust-gui" };            app.reset(SailfishApp::application(argc, argv));            view.reset(SailfishApp::createView());            view->engine()->addImportPath("/usr/share/aurora-rust-gui/qml");        }    };}}cpp_class!(    pub unsafe struct SailfishApp as "SailfishAppHolder");impl SailfishApp {    /// Creates a new SailfishApp.    pub fn new() -> Self {        cpp!(unsafe [] -> SailfishApp as "SailfishAppHolder" {            qDebug() << "SailfishApp::new()";            return SailfishAppHolder();        })    }    /// Sets the main QML (see QQuickView::setSource for details).    pub fn set_source(&mut self, url: QString) {        cpp!(unsafe [self as "SailfishAppHolder *", url as "QString"] {            const auto full_url = QString("/usr/share/aurora-rust-gui/qml/%1").arg(url);            qDebug() << "SailfishApp::set_source()" << full_url;            self->view->setSource(full_url);        });    }    /// Shows the main view.    pub fn show(&self) {        cpp!(unsafe [self as "SailfishAppHolder *"] {            qDebug() << "SailfishApp::show()";            self->view->showFullScreen();        })    }    /// Launches the application.    pub fn exec(&self) {        cpp!(unsafe [self as "SailfishAppHolder *"] {            qDebug() << "SailfishApp::exec()";            self->app->exec();        })    }}

Биндинги для используемых классов QByteArray, QString, QUrl копируем из того же qmetaobject и расфасовываемым по отдельным файлам. Здесь приводить их не буду, если что, исходники можно посмотреть в репозитории на GitHub.

Немного скорректируем заголовочный файл sailfishapp.h, чтобы он искал заголовочные файлы Qt в правильных местах:

include/sailfishapp/sailfishapp.h:

// ...#ifdef QT_QML_DEBUG#include <QtQuick>#endif#include <QtCore/QtGlobal>  // Было `#include <QtGlobal>`#include <QtCore/QUrl>      // Было `#include <QUrl>`class QGuiApplication;class QQuickView;class QString;// ...

Осталось только добавить файлы QML и положить их в дистрибутив RPM.

все здесь

qml/main.qml:

import QtQuick 2.6import Sailfish.Silica 1.0ApplicationWindow {    cover: Qt.resolvedUrl("cover.qml")    initialPage: Page {        allowedOrientations: Orientation.LandscapeMask        Label {            anchors.centerIn: parent            text: "Hello, Aurora!"        }    }}

qml/cover.qml:

import QtQuick 2.6import Sailfish.Silica 1.0CoverBackground {    Rectangle {        id: background        anchors.fill: parent        color: "blue"        Label {            id: label            anchors.centerIn: parent            text: "Rust GUI"            color: "white"        }    }    CoverActionList {        id: coverAction        CoverAction {            iconSource: "image://theme/icon-cover-cancel"            onTriggered: Qt.quit()        }    }}

.rpm/aurora-rust-gui.spec:

# ...Source3: qml# ...%install# ...mkdir -p %{buildroot}%{_datadir}/%{name}cp -ra %{SOURCE3} %{buildroot}%{_datadir}/%{name}/qml%cleanrm -rf %{buildroot}%files# ...%{_datadir}/%{name}/qml

Makefile:

# ...rpm:# ...@cp -rvf qml ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES# ...

Собираем:

make cleanmake releasemake rpm

Подписываем, копируем, устанавливаем, запускаем из командной строки и вуаля:

$ devel-suPassword:# pkcon install-local ./aurora-rust-gui-0.1.0-1.armv7hl.rpmInstalling filesTesting changesFinishedInstalling filesStartingResolving dependenciesInstalling packagesDownloading packagesInstalling packagesFinishedDownloaded  aurora-rust-gui-0.1.0-1.armv7hl (PK_TMP_DIR)         Rust GUI example for Aurora OSInstalled   aurora-rust-gui-0.1.0-1.armv7hl (PK_TMP_DIR)            Rust GUI example for Aurora OS# exit$ aurora-rust-gui[D] __cpp_closure_14219197022164792912_impl:33 - SailfishApp::new()[D] SailfishAppHolder::SailfishAppHolder:15 - SailfishAppHolder::SailfishAppHolder()[D] unknown:0 - Using Wayland-EGLlibrary "libpq_cust_base.so" not found[D] __cpp_closure_16802020016530731597:42 - SailfishApp::set_source() "/usr/share/aurora-rust-gui/qml/main.qml"[W] unknown:0 - Could not find any zN.M subdirs![W] unknown:0 - Theme dir "/usr/share/themes/sailfish-default/meegotouch/z1.0/" does not exist[W] unknown:0 - Theme dir "/usr/share/themes/sailfish-default/meegotouch/" does not exist[D] onCompleted:432 - Warning: specifying an object instance for initialPage is sub-optimal - prefer to use a Component[D] __cpp_closure_12585295123509486988:50 - SailfishApp::show()[D] __cpp_closure_15029454612933909268:59 - SailfishApp::exec()

Вот так выглядит наше приложение с разных ракурсов:

 Рабочий стол с ярлыком Рабочий стол с ярлыком Главное окно приложенияГлавное окно приложения Панель задач Панель задач

Последние штрихи

Приложение отлично стартует из командной строки при подключении по SSH, однако никак не реагирует при попытке запуска с помощью ярлыка. Путём некоторых экспериментов удалось установить, что для этого надо экспортировать символ main (RPM-валидатор выдавал предупреждение на этот счёт, но некритичное).

Серия проб и ошибок показала, что надо добавить ещё один ключ линкера: -export-dynamic.

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o -rpath lib --dynamic-linker /lib/ld-linux-armhf.so.3 -export-dynamic"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

После этого всё работает так, как и ожидается.

Заключение

Понятно, что до того, как использовать Rust в проде, ещё надо решить немало вопросов. Как минимум, я предвижу сложности с дополнительными зависимостями при подключении новых крейтов, извечные танцы с бубном вокруг сегфолтов при FFI-вызовах, увязывание систем владения Qt и Rust. Некоторые интересные подробности можно почерпнуть из статьи от автора qmetaobject-rs. Наверняка, время от времени будут всплывать и другие проблемы.

Плюс к этому, для того, чтобы использовать Qt-овские классы, к каждому из них необходимо писать биндинги.

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

Буду рад вопросам и замечаниям в комментариях. И ставьте лайк, подписывайтесь на канал :-)

Подробнее..

Кроссплатформенная мобильная разработка история вопроса

04.03.2021 12:16:30 | Автор: admin

Когда речь заходит о разработке сразу для Android и iOS, начинаются холивары и гадания на кофейной гуще. Что перспективнее, Flutter или Kotlin Multiplatform? За этими технологиями будущее, или завтра их обе забудут?

Уверенно делать прогнозы по принципу я так вижу занятие весёлое, но бессмысленное. Как подойти конструктивнее? Как известно, кто забывает об истории, обречён на её повторение. Поэтому я решил вспомнить, какими были предыдущие решения Android+iOS, и посмотреть, что с ними стало. Тогда вместо голых спекуляций будут суровые факты. И эти факты из прошлого полезны для понимания будущего: они показывают, где разложены грабли.

Я не использовал лично большинство старых технологий, но благодаря общению со спикерами нашей конференции Mobius узнал многое об их опыте, и текст во многом основан на этом. Если вы работали с чем-то из перечисленного, смело дополняйте в комментариях, будет интересно и полезно.

Оглавление


Web / PWA

В 2007-м, представляя разработчикам первый iPhone, Стив Джобс объяснял, что нативные приложения не нужны: В iPhone есть полный движок Safari. Так что можете писать потрясающие Web 2.0 и Ajax-приложения, которые выглядят и действуют на iPhone именно как приложения.

Android на тот момент ещё даже не был анонсирован. Но получается, что исторически первым единым решением для iOS и Android стал веб.

Вот только разработчики тогда не разделили энтузиазма Джобса (неудивительно, учитывая тогдашнее состояние мобильного интернета). И годом позже всё-таки появился App Store для нативных приложений. А его комиссия в 30% стала новым денежным потоком для компании. В итоге её позиция сменилась на противоположную: теперь Apple считает, что правильный подход это натив (и предпочитает не вспоминать, что там её лидер говорил в 2007-м, Океания всегда воевала с Остазией).

Однако идея веб-приложений не исчезла, а продолжила развиваться. И в 2015-м новое поколение таких приложений назвали Progressive Web Apps. Они могут хранить данные локально, работать в офлайне, а ещё их можно установить на домашний экран смартфона. Чем это тогда не настоящие мобильные приложения? Что ещё для счастья надо?

Ну, например, для счастья нужны push-уведомления. По состоянию на 2021-й iOS не поддерживает их у PWA, и это мощнейший стопор для распространения подхода. Получается, компания, которая первой хвалила веб-приложения, позже сама поставила главное препятствие на их пути. На change.org есть даже петиция, обращённая к Apple: мы всё понимаем, вы дорожите своими 30%, но эта ситуация должна измениться.

Что в итоге: подход жив, но не стал общепринятым во многом из-за ограничений Apple. В будущем что-то может измениться, стоит следить.


PhoneGap/Apache Cordova

Это решение тоже связано с вебом, но подход другой так называемый гибридный. Тут предложили использовать знакомую троицу HTML/CSS/JS, но результат не публиковать в виде сайта, а упаковать в контейнер нативного приложения. В таком случае не сталкиваешься с ограничениями веба и можешь реализовать те же push-уведомления.

PhoneGap появился в 2009-м благодаря компании Nitobi, а в 2011-м Adobe купила её и создала также опенсорсный вариант Apache Cordova. У проекта модульная архитектура, позволяющая подключать плагины. И в сочетании с опенсорсностью это означает, что если Cordova не умеет чего-то нужного (например, взаимодействовать с акселерометром смартфона), сообщество может научить. Вроде как прекрасно, да?

На практике что-то не очень. В своё время некоторая популярность у проекта была, те же плагины появлялись, но масштабного захвата рынка не произошло. А затем всё и вовсе пошло на спад.

Почему так? Среди главных причин называют UX, уступающий нативным приложениям. Всё хуже и с производительностью, и с плавностью. Но основное отличие даже не измерить числами: пользователю попросту заметна разница между гибридным приложением и нативным, они производят разное впечатление. Для людей через WebView ощущения не те.

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

Интересно, что авторы проекта сами предвидели такое развитие событий и ещё в 2012-м написали, что итоговая цель PhoneGap прекратить своё существование. И недавно эта цель была достигнута: в 2020-м Adobe заявили о прекращении разработки, ссылаясь на то, что нишу закрыли PWA.

Что в итоге: разработка прекращена.


Qt

Проект Qt помогал людям заниматься кроссплатформенной разработкой ещё с 90-х, когда речь шла о десктопных ОС, а не мобильных. При этом он завязан на C++, который для Android и iOS не совсем чужой: даже нативные разработчики на этих платформах могут обращаться к плюсам. Так что, казалось бы, поддержка iOS/Android со стороны Qt просто напрашивалась.

Но поначалу дело осложнялось тем, что в 2008-м проект купила Nokia. Компания тогда делала ставку на Symbian и не горела желанием помогать конкурентам. В 2010-м возможностью запускать Qt-приложения на Android занимались энтузиасты, и на Хабре об этом писали:

В 2011-м Nokia отказалась от Symbian в пользу Windows Phone, а часть проекта Qt продала компании Digia. И тогда началась работа над поддержкой одновременно Windows 8, Android и iOS. Ну вот теперь-то счастье? Спустя 10 лет ясно, что тоже нет.

Вакансии по мобильной разработке на Qt сейчас единичные. Хабрапосты о ней появляются очень редко и не свидетельствуют об успехе: в одном причиной использования Qt стала ОС Аврора (экс-Sailfish), в другом попросту у меня уже был большой опыт с ним.

Что помешало? Я встречал жалобы на то, что недостаточно было использовать сам Qt и писать на С++/QML. Потому что средствами Qt многое было не реализовать, и приходилось-таки иметь дело с конкретной платформой (например, на Android работать с Java, увязывая это с плюсовой частью через JNI). Всё это очень неудобно и подрывает исходную идею бодренько запилим два приложения по цене одного.

При этом здесь пользователь тоже ощущает не-нативность. А к IDE Qt Creator есть нарекания, рантайм Qt увеличивает размер приложения, бесплатный вариант Qt подходит не всегда и может понадобиться недешёвый коммерческий. Кроме того, мне лично кажется, что ещё сказался язык. Кроссплатформенной разработке желательно быть такой, чтобы нативным разработчикам было попроще перекатиться туда, а с языком C++ слово попроще не ассоциируется.

И хотя случаи использования Qt встречаются до сих пор, это скорее исключения из правил, у которых могут быть какие-то особые исходные условия: например, хотим перетащить на телефоны с десктопа уже имеющуюся кодовую базу на C++.

Что в итоге: крайне нишевое решение, которое не умерло полностью, но используется очень узкой прослойкой.


Xamarin

Мигель де Икаса ещё в проекте Mono занимался тем, что притаскивал .NET на несвойственные ему платформы (начав с Linux). А когда в 2011-м он вместе со всей командой Mono попал под сокращение, основал новую компанию Xamarin, собрал прежних коллег там, и сосредоточился на мобильных платформах: мол, давайте писать мобильные приложения для них на C#, вот инструмент для этого.

Надо понимать контекст того времени. Годом ранее компания Microsoft выпустила Windows Phone и стремилась стать третьим большим игроком на мобильном рынке. И даже большая Windows лезла на мобильный рынок: готовилась к выходу Windows 8, оптимизированная для планшетов.

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

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

Ещё порой вызывали недовольство и то, что после появления новых фич в Android/iOS приходилось ждать их поддержки в Xamarin, и стоимость лицензии, и общее состояние экосистемы (документация, поддержка, библиотеки).

А тем временем компания Microsoft возлюбила опенсорс и сторонние платформы вроде Linux, так что идеи де Икасы оказались ей близки, и в итоге она купила Xamarin. Теперь его наработки вошли в .NET 5, и в прошлом году представили .NET MAUI (Multi-platform App UI) развитие тулкита Xamarin.Forms. В общем, не забросили купленное.

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


React Native

Наконец, самое свежее. В 2013-м в Facebook выпустил React, ставший очень успешным во фронтенде, а в 2015-м за ним последовал React Native, приносящий подход в мобильную разработку.

Писать предлагается на JavaScript, поэтому кому-то может показаться, что это реинкарнация PhoneGap и его HTML-подхода, но нет. Когда-то Facebook действительно полагался для мобильных устройств на HTML, но ещё в 2012-м Цукерберг назвал это ошибкой. И у React Native идея не в том, чтобы с помощью HTML/CSS городить что хочешь, а в том, чтобы с помощью JSX использовать нативные элементы UI так что пользователь ощущал себя прямо как с нативным приложением.

Насколько это получилось? Шероховатости и срезанные углы существуют с ними сталкиваются и разработчики, и пользователи. Для кого-то они оказываются критичными: особенно нашумел пост Airbnb, где после React Native решили вернуться к нативной разработке. Но какие-то другие компании (вроде самой Facebook) эти ограничения не остановили, использование в индустрии есть.

Поскольку нативной отполированности не достичь, это решение часто рекомендуют для случаев, где она и не требуется. Если пользователь залипает в приложении часами (как в соцсетях), то любая неаккуратная мелочь будет бросаться ему в глаза но вот если юзкейс приложения такой, что его включают на минуту в неделю, то эти мелочи просто никто не заметит.

Если зайти на HeadHunter и сравнить число вакансий по React Native с нативной разработкой, то их немного. Но вот если сравнивать с Qt/Xamarin/PhoneGap то вакансий окажется больше, чем у них всех, вместе взятых, это наиболее успешный вариант.

Что в итоге: React Native не стал так успешен, как его фронтендовый старший брат, но определённую нишу занял.


Выводы

Обычно в холиварах занимают крайние позиции: эта крутая штука всех победит или все эти глупости скоро отомрут. А у меня по итогам всех вариантов позиция получается сдержанной:

  • Победит и умрёт не единственные варианты. Есть ситуации, когда технология занимает небольшой сегмент рынка и не претендует на мировое господство, но приносит кому-то пользу.

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

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

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

А вот про Kotlin Multiplatform сделать выводы по прошлому у меня не получилось, потому что у него нетипичная ситуация, отличающаяся от предшественников. Во-первых, идея кроссплатформа годится не для всего тут заложена прямо в фундамент: а мы и не предлагаем объединять всё, реализуйте общую бизнес-логику, а остальное на Android и iOS делайте раздельно. А во-вторых, тут играет на руку родной для Android язык: он уже знаком половине мобильных разработчиков, и такого в кроссплатформе раньше не возникало. Так что опыт предыдущих технологий тут непоказателен остаётся смотреть, что покажет время.

На последнем Mobius было сразу несколько кроссплатформенных докладов (три про Flutter, один про Kotlin Multiplatform) мы уже выложили все видеозаписи конференции на YouTube, так что можете сами их посмотреть. А мы тем временем вовсю работаем над следующим Mobius: он пройдёт 13-16 апреля в онлайне, и там без освещения кроссплатформы тоже не обойдётся. Так что, если вас заинтересовал этот пост, то вам будет интересно и там.

Подробнее..

Как мы вырастили и победили читеров в своем онлайн-шутере

02.03.2021 22:15:14 | Автор: admin

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

Несколько лет назад мы написали тестовый прототип шутера, который быстро стал популярным защиты никакой не было, и некоторые побежали этим пользоваться. Мы затыкали пробоины скотчем, читеры находили новые дыры, а мы учились у них с помощью социальной инженерии и повышения технических навыков. И в итоге пересмотрели отношение к защите будущих мобильных проектов (об этом в конце статьи).

До появления мобильной Pixel Gun 3D в 2013 году команда Lightmap писала мини-игры на движке Cocos2d. То есть опыта ни в трехмерных, ни тем более мультиплеерных играх у нас тогда не было. Мы посещали конференции, очень много общались с другими разработчиками и, посмотрев на их опыт, все-таки решили попробовать себя в 3D на Unity.

Изучать движок решили на прототипе шутера, чтобы посмотреть, как отреагируют игроки. В итоге проект выстрелил так, что собирать опытную команду (а на старте было всего 3 разработчика) и разрабатывать качественный продукт с нуля уже не было времени. Всему пришлось учиться на своих ошибках после релиза.

Читеры из младших классов

С самого начала база игроков росла достаточно быстро, а с ними приходили первые читеры. Но из-за ограниченных ресурсов в приоритете стояли совсем другие задачи на старте было мало контента, игровых механик, да и цельной меты как таковой не было. Только когда в обзорах на YouTube слишком часто стали всплывать упоминания о взломах, пришлось заняться этим вопросом.

То есть изначально мы даже не рассматривали проблему появления читеров, а думали только о том, как сделать интересную мобильную игру на Unity. Возможно это и была первая ошибка, но напомню, что мы впервые собирали подобный прототип.

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

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

Сразу же решили передать хранение прогресса в защищенное зашифрованное значение и переделать все переменные, в которых хранились ответственные данные, на классы, где для скрытия используется рандомная соль. Не обошлось без факапов.

В месте, где валюта переносилась из старого хранилища в новое, мы допустили баг, и когда пользователь, который не входил в игру больше недели, обновлялся до новой версии то вся его валюта обнулялась. Самым сложным было понять, в чем дело, потому что при наших тестах такой сценарий не воспроизводился. Оказалось, что место отправки аналитики входа в игру после долгого отсутствия (в которой косвенно использовалась валюта по коду) располагалось раньше нашей миграции.

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

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

Читеры постарше

Они начали менять код приложения, дописывать свои менюшки для взлома различных параметров, переподписывать и выкладывать apk, которые пользовались успехом на форумах.

Все старания спрятать в коде проверку подписи быстро находились и вырезались, как и другие проверки на взломы приложения. Для обфускации мы стали использовать появившуюся в Unity IL2cpp, но в итоге выяснилось, что движок хранит в проекте дамп всех методов с адресами памяти чем стали пользоваться уже старшеклассники.

Ситуация по сути не менялась, так как мы все время боролись с симптомами, а не с болезнью. Даже не смотря на то, что иногда нам удавалось закрыть уязвимости еще до их использования для этого приходилось выдавать себя за тех самых читеров.

Социальная инженерия против читеров

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

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

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

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

А тут случился еще один серьезный факап с нашей стороны. В сеть утекла версия для разработчиков годовалой давности с дев-консолью, в которой можно было легко намешать любую сущность. К этом моменту CCU в пике доходил до 70 тысяч игроков, а DAU стабильно переваливал за миллион пользователей.

Стало понятно, что дальнейшее развитие приложения невозможно при таком состоянии дел со взломами. Все старания с поднятием уровня основных метрик нивелировались, а часто даже занижались вывешенными взломанными версиями.

Приоритетной задачей стало: убрать возможность читерства из приложения.

Решение

Итак, что мы имели на входе:

  • Огромное приложение с несколькими годами активной разработки и большим легаси.

  • Возможность изменения версии и выкладки пользователям.

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

  • Синхронизация прогресса между девайсами через сторонние сервисы прогресс можно было беспрепятственно суммировать между игроками.

  • Отсутствие надежной возможности бана читера в измененных версиях его тоже вырезали.

  • Отсутствие надежного отслеживания фактов читерства.

  • В качестве игрового сервера Photon Unity Networking (Cloud), который не имеет серверной логики и, следовательно, не дает возможности контролировать игровую логику.

  • Наличие измененных версий игры в китайских сторонних сторах.

  • Связь с сервером через www-запросы, которые легко отследить и подделать.

  • Возможность подставить из кода любой id, который мы не сможем идентифицировать как подставной.

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

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

Началась масштабная работа, для чего практически удвоили команду разработчиков, привлекая их с другого проекта.

Пока кратко о том, что мы сделали для перелома ситуации с читерами:

  1. Для обфускации ввели новые стандарты написания кода, позволяющие плагину обфускации сделать свое дело, и переписали под них все приложение. Задача не особо сложная, но ресурсоемкая, так как кодовая база к этому времени уже была довольно большой.

  2. Переработали хранение данных еще более глобальная и ресурсоемкая задача. Сущностей к этому времени было несчетное количество, данные сохранялись беспорядочно в произвольных форматах, что никак нам не подходило. Для синхронизации прогресса с сервером реализовали постоянное сокетное соединение, переведя на него все функционалы, связанные с метой игры (ну и, само собой, сам сервер, но это отдельная история). Для игроков оставили возможность поиграть оффлайн в несколько режимов, правда без наград и изменения прогресса.

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

  4. Реализовали надежную систему бана, которую нельзя было вырезать из клиента, так как у забаненного игрока блочится весь серверный функционал (при том что большая часть логики была перенесена на сервер).

  5. Добавили подсчет хеша всех библиотек и начали сравнивать его с разрешенными при авторизации на сервере, куда они добавляются при сборке релизных билдов.

  6. Расставили в дополнительные места защиту от переподписывания версий, лаунчеров (на Android), твиков (на iOS), спрятав уже в обфусцированном коде.

  7. Для предотвращения взлома игровых параметров серверной логикой ввели Photon Plugin, доступный на тарифе Enterprise Cloud. Он позволяет мониторить пересылаемый между пользователями игровой трафик, чтобы вычислять тех, у кого действия выходят за рамки допустимого (жизни, урон, скорость перемещения/стрельбы, использование запрещенных предметов и так далее). Для возможности отслеживания взломов переписали сетевое взаимодействие игроков.

  8. Добавили серверную валидацию инапов.

  9. Добавили защиту от взлома оперативной памяти.

  10. Практически одномоментно выкатили все в продакшен.

  11. Продолжили реализовывать и улучшать собственную систему аналитики.

Сейчас аналитика основной инструмент постоянного отслеживания попыток взлома. Мы можем отслеживать все действия каждого конкретного пользователя и реагировать даже на единичные попытки взлома, не давая распространиться лазейкам. А когда-то эту роль для команды играли YouTube и профильные сайты.

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

ААА-защита для мобильных игр

Сейчас игра надежно защищена от взломов, у нее, как минимум, нет распространенных методов взлома. Бывают единичные, но их оперативно отслеживаем и реагируем.

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

Также всегда проще писать сразу под необходимые меры защиты, а не переделывать проект под них в будущем.

На текущий момент наша программа-минимум для новых проектов это:

  • Плагин обфускации для осложнения понимания кода приложения.

  • Хранение и синхронизация прогресса через наш сервер с использованием валидации всех основных операций.

  • Зашифрованное хранилище на диске, чтобы исключить перенос данных от девайса к девайсу копированием данных.

  • Надежная система бана.

  • Photon Plugin, как минимум, для валидации действий пользователя.

  • Защита от изменения, переподписывания версий, лаунчеров и твиков.

  • Серверная валидация инапов.

  • Повсеместное использование засоленных данных как защита от изменений в оперативной памяти.

  • Собственная система аналитики с отслеживанием подозрительных действий конкретных пользователей.

И продолжаем сидеть на форумах под видом читеров, общаться и мониторить новые уязвимости. Например, одна из последних это скрытие использования тиков после выхода джейлбрейка на свежие версии iOS. Об этом тоже узнали из форумов, изучили механизмы работы и оперативно выкатили обновление но об этом и других кейсах уже в следующий раз.

Подробнее..

Распознание блоков текста в IOS-приложении с помощью Vision

18.02.2021 14:23:54 | Автор: admin

Работая над приложением, связанным с финансовыми операциями, возникла необходимость распознать и выделить суммы на чеках. Начиная с 13-ой версии в IOS-разработке появился нативный фреймворк Vision, который позволяет распознавать различные объекты на изображениях, без задействования сторонних сервисов.
В данной статье представлен личный опыт разработки приложения, использующего Vision.

Что такое Vision

Из документации Apple: "Vision применяет алгоритмы "компьютерного зрения" для выполнения множества задач с входными изображениями и видео. Фреймворк Vision выполняет распознание лиц, обнаружение текста, распознавание штрих-кодов, регистрацию изображений. Vision также позволяет использовать пользовательские модели CoreML для таких задач, как классификация или обнаружение объектов."
Анализируя документацию Apple, можно предположить, что Vision - это один из этапов подготовки таких продуктов как Apple glasses или шлем смешанной реальности. Забегая вперед, следует подчеркнуть, что данный фреймворк потребляет изрядное количество ресурсов. Обработка статичного изображения может занимать десятки секунд, следовательно, работа с видео в реальном времени будет предельно ресурсоемким процессом, над оптимизацией которого инженерам Apple еще предстоит поработать.
В рамках поставленной задачи, необходимо было решить следующую проблему: распознание блоков текста с помощью Vision.

Разработка

Проект построен на UIKit, который в данной статье детально рассматриваться не будет. Основное внимание уделяется блокам кода, связанным с фреймворком Vision. Приведенные листинги снабжены комментариями, позволяющими разработчикам детальнее понять принцип работы с фреймворком.
В MainViewController, который будет взаимодействовать с фреймворком Vision, нужно объявить две переменные:

//Recognition queuelet textRecognitionWorkQueue = DispatchQueue(label: "TextRecognitionQueue", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)//Request for text recognitionvar textRecognitionRequest: VNRecognizeTextRequest?
  1. Очередь для задач Vision не вызывает никаких затруднений у разработчиков. Именно в ней будут выполняться все задачи фреймворка.

  2. Объявляется переменная типа VNRecognizeTextRequet. Инициализируется объект из ViewDidLoad (или из init), так как он должен быть активен на протяжении всей жизни ViewController. Этот объект отвечает за работу с Vision, поэтому необходимо разобрать его инициализацию подробнее:

//Set textRecognitionRequest from ViewDidLoadfunc setTextRequest() {    textRecognitionRequest = VNRecognizeTextRequest { request, error in        guard let observations = request.results as? [VNRecognizedTextObservation] else {            return        }        var detectedText = ""        self.textBlocks.removeAll()                    for observation in observations {            guard let topCandidate = observation.topCandidates(1).first else { continue }            detectedText += "\(topCandidate.string)\n"                        //Text block specific for this project            if let recognizedBlock = self.getRecognizedDoubleBlock(topCandidate: topCandidate.string, observationBox: observation.boundingBox) {                self.textBlocks.append(recognizedBlock)            }        }                    DispatchQueue.main.async {            self.textView.text = detectedText            self.removeLoader()            self.drawRecognizedBlocks()        }    }            //Individual recognition request settings    textRecognitionRequest!.minimumTextHeight = 0.011 // Lower = better quality    textRecognitionRequest!.recognitionLevel = .accurate}

Настройки объекта textRecognitionRequest. Описание всех доступных настроек можно найти в документации. Наиболее важным является параметр minimumTextHeight. Именно этот параметр отвечает за сочетание быстродействия и точности распознания текста. Для каждого проекта необходимо найти индивидуальное значение данного параметра, оно зависит от того, какие данные будет обрабатывать приложение.
Так как основной поставленной задачей являлось считывание текста с квитанций, для вычисления значения параметра minimumTextHeight в приложение были добавлены различные типы квитанций в различном состоянии (в том числе и основательно помятые). В результате тестирования было определено значение равное 0.011. В случае распознания текста с квитанций, это значение лучшим образом сочетает в себе быстродействие и точность. Однако нужно отметить, что текст с одного изображения распознается в среднем за пять секунд. Подобной скорости недостаточно для обработки информации в реальном времени и ее следует значительно оптимизировать инженерам Apple.
На основе представленного кода можно сделать вывод, что после операции распознания, объект типа VNRecognizeTextRequet получает блоки текста. Именно с ними и ведется дальнейшая работа, в зависимости от функций приложения. В рассматриваемом примере, каждый распознанный фрагмент текста был внесен в текстовое поле. Так как особенностью задействованного приложения является выделение суммы на квитанции, следовательно, сохранялись только блоки текста, которые можно преобразовать в тип Double. Помимо распознанного текстового значения сохраняются и координаты блока текста на изображении.
Представленный ниже метод отвечает за запуск работы запроса на распознание:

//Call text recognition request handlerfunc recognizeImage(cgImage: CGImage) {    textRecognitionWorkQueue.async {        let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])        do {            try requestHandler.perform([self.textRecognitionRequest!])        } catch {            DispatchQueue.main.async {                self.removeLoader()                print(error)            }        }    }}

В метод передается объект CGImage, в котором необходимо распознать текст. Вся работа по распознанию ведется в созданной для этого очереди. Создается объект VNImageRequestHandler, в который передается распознаваемый объект CGImage. В блоке do/try/catch запускается работа инициализированного объекта типа VNRecognizeTextRequet.
Описанные выше функции отвечают за распознание текста в приложении. Однако стоит еще остановится на методах, связанных с выделением нужных блоков текста.

func drawRecognizedBlocks() {    guard let image = invoiceImage?.image else  { return }        //transform from documentation    let imageTransform = CGAffineTransform.identity.scaledBy(x: 1, y: -1).translatedBy(x: 0, y: -image.size.height).scaledBy(x: image.size.width, y: image.size.height)            //drawing rects on cgimage    UIGraphicsBeginImageContextWithOptions(image.size, false, 1.0)    let context = UIGraphicsGetCurrentContext()!    image.draw(in: CGRect(origin: .zero, size: image.size))    context.setStrokeColor(CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1))    context.setLineWidth(4)        for index in 0 ..< textBlocks.count {        let optimizedRect = textBlocks[index].recognizedRect.applying(imageTransform)        context.addRect(optimizedRect)        textBlocks[index].imageRect = optimizedRect    }    context.strokePath()            let result = UIGraphicsGetImageFromCurrentImageContext()    UIGraphicsEndImageContext()    invoiceImage?.image = result}

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

struct RecognizedTextBlock {    let doubleValue: Double    let recognizedRect: CGRect    var imageRect: CGRect = .zero}

При распознании блоков текста фреймворк Vision вычисляет ряд важных параметров в объекте VNRecognizedTextObservation. Для нужд рассматриваемого проекта необходимо было получить только значение типа Double и его координаты на изображении, сохраняемые в константе recognizedRect.
Для выделения блока текста на изображении, следует применить трансформацию к координатам из константы recognizedRect. Полученные координаты так же сохраняются в объекте RecognizedTextBlock в переменной imageRect, необходимой для обработки нажатий на выделенные блоки текста.
После сохранения точных координат выделяемых блоков на изображении, обработку нажатий на выделенные области можно осуществить несколькими способами:

  • Добавить необходимое количество невидимых кнопок на изображение, при помощи трансформации сохраненного объекта imageRect;

  • При каждом нажатии на изображение проверять массив блоков текста и искать совпадение координат нажатия с сохраненным объектом imageRect и др.

Чтобы не перегружать ViewController дополнительными элементами, был использован второй способ.

//UIImageView tap listener@objc func onImageViewTap(sender: UITapGestureRecognizer) {    guard let invoiceImage = invoiceImage, let image = invoiceImage.image else {        return    }            //get tap coordinates on image    let tapX = sender.location(in: invoiceImage).x    let tapY = sender.location(in: invoiceImage).y    let xRatio = image.size.width / invoiceImage.bounds.width    let yRatio = image.size.height / invoiceImage.bounds.height    let imageXPoint = tapX * xRatio    let imageYPoint = tapY * yRatio    //detecting if one of text blocks tapped    for block in textBlocks {        if block.imageRect.contains(CGPoint(x: imageXPoint, y: imageYPoint)) {            showTapAlert(doubleValue: block.doubleValue)            break        }    }}

Использование представленного метода позволяет вычислить координаты нажатия на объект ImageView с сохранением его пропорций и поиска полученных координат в массиве сохранных распознанных блоков текста.

Выводы

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

Приложение распознающее блоки текста с помощью VisionПриложение распознающее блоки текста с помощью Vision

Для ознакомления проект можно скачать из репозитория.

Подробнее..

ZERG что за зверь?

19.02.2021 14:12:20 | Автор: admin


Когда мы говорим о CI&CD, мы часто углубляемся в базовые инструменты автоматизации сборки, тестирования и доставки приложения фокусируемся на инструментах, но забываем осветить процессы, которые протекают во время отрезания и стабилизации релизов. Однако, не все готовые инструменты одинаково полезны, а какие-то кастомные процессы не укладываются в их покрытие. Приходится исследовать процессы и находить пути автоматизации для их оптимизации.

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

У нас есть ночные прогоны, когда гоняются полные наборы тестов. Но на самой заре освоения Zephyr, нашим тестировщикам во время регресса приходилось скачивать xcresult, или ещё ранее plist, или junit xml, а затем проставлять соответствия зелёных и красных тестов в зефире руками. Это довольно рутинная операция, да и занимает она много времени, чтобы руками пройти 500-600 тестов. Такие вещи хочется отдать на откуп бездушной машине. Так родился ZERG.


Рождение зерга


Zephyr Enterprise Report Generator небольшая утилита, которая изначально умела только искать соответствия в отчёте тестов и отправлять в Zephyr их актуальные статусы. Позже утилита получила новые функции, но сегодня мы остановимся на поиске и отправке отчётов.
В Zephyr нам предлагается оперировать версиями, циклами и проходами (execution) тест кейсов. Каждая версия содержит произвольное количество циклов, а каждый цикл содержит в себе проходы кейсов. Такие проходы содержат в себе информацию о задаче (zephyr прекрасно интегрируется с jira и тест кейс это, по сути, задачка в jira), авторе, о статусе кейса, а также о том, кто занимается этим кейсом и о других необходимых деталях.
Для автоматизации проблемы, которую мы обозначили выше, нам важно разобраться в проставлении статуса кейса.

Работа с кодом


Но как соотнести тест в коде и тест в зефире? Здесь мы выбрали довольно простой и прямолинейный подход: для каждого теста мы добавляем в секцию комментариев ссылки на задачи в jira.


В комментариях также могут размещаться дополнительные параметры, но об этом позже.
Так, один тест в коде может покрывать несколько задач. Но работает и обратная логика. Для одной задачи может быть написано несколько тестов в коде. Их статусы будут учитываться при составлении отчёта.
Нам надо пройти по исходникам, извлечь все тест-классы и тесты, слинковать задачи с методами и соотнести это с отчётом о прохождении тестов ( xcresult или junit ).
Работать с самим кодом можно разными путями:
просто читать файлы и через регулярные выражения извлекать информацию
использовать SourceKit

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



Нам надо получить тесты. Для этого опишем структуры:



Затем нам надо прочитать отчёт о прохождении тестов. ZERG был рождён ещё до переезда на xcresult, и поэтому умеет парсить plist и junit. Детали в этой статье нас всё ещё не интересуют, они будут приложены в коде. Поэтому отгородимся протоколам



Остается только слинковать тесты в коде с результатами тестов из отчетов.



Проверяем соответствие по имени класса и имени теста (в разных классах могут быть методы с одинаковыми именами) и нужен ли рефакторинг для теста. Если он нужен, то считаем, что он ненадёжный и выкидываем из статистики.

Работаем с зефиром


Теперь, когда мы прочитали отчёты о тестировании, нам надо их перевести в контекст zephyr. Для этого надо получить список версий проекта, соотнести с версией приложения (чтобы это так работало, необходимо, чтобы версия в зефире совпадала с версией в Info.plist вашего приложения, например, 2.56), выкачать циклы и проходы. А дальше соотнести проходы с нашими уже имеющимися отчётами.
Для этого нам надо реализовать в ZephyrAPI следующие методы:



Cпецификацию можно увидеть здесь: getzephyr.docs.apiary.io, а реализацию клиента в нашем репозитории.
Общий алгоритм довольно простой:



На этапе сопоставления проходов с отчётами есть тонкий момент, который необходимо учитывать: в zephyr api обновление execution отправлять удобнее всего пачками, где передаётся общий статус и список идентификаторов проходов. Нам нужно развернуть наши отчёты относительно тикетов и учесть n-m соотношение. Для одного кейса в зефире может быть несколько тестов в коде. Один тест в коде может покрывать несколько кейсов. Если для одного кейса есть n тестов в коде и один из них красный, то для такого кейса общий статус красный, однако если один из таких тестов покрывает m кейсов и он зелёный, то остальные кейсы не должны стать красными.
Поэтому мы оперируем сетами и ищем пересечение красных и зелёных. Всё, что попадает в пересечение, мы отнимаем из зелёных результатов и отправляем отредактированные сведения в zephyr.



Здесь ещё нужно отметить, что внутри команды мы договорились, что zerg не будет менять статус прохода, если:
1. Текущий статус blocked или failed (раньше для failed мы меняли статус, но сейчас отказались от практики, потому что хотим, чтобы тестировщики обращали внимание на красные автотесты во время регресса).
2. Если текущий статус pass и его поставил человек, а не zerg.
3. Если тест помечен как флакающий.

Интересности Zephyr API


При запросе циклов нам приходит json, который с первого взгляда невозможно систематизировать для десериализации. Всё дело в том, что запрос на получение циклов для версии, хотя по смыслу и должен возвращать массив циклов, на самом деле возвращает один объект, где каждое поле уникально и называется идентификатором цикла, который лежит в значении. Чтобы такое обработать, мы применяем несложные хаки:



Статусы прохождения тестов приходят в одном из запросов рядом с объектом запроса. Но их можно вынести заранее в enum:



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



Вместо заключения


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

Что вам даст учеба у практиков, или почему наш курс по Flutter это про реальные проекты и навыки для работы

19.02.2021 18:05:42 | Автор: admin

Вот уже третий год мы разрабатываем на Flutter. Сделали на нём кроссплатформенные приложения для Росбанка, сети аптек Ригла, ресторанов KFC, в разработке ещё много проектов. Буквально на наших глазах Flutter из нишевой технологии стал мощным игроком, который теснит не только React Native, но и нативную разработку.

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

Спрос на Flutter-разработчиков очень быстро растёт. Не хватает даже джунов, а у миддлов и сеньоров услуги стоят очень дорого. Толковые ребята нужны и нам.

Команда flutter в Surf собрала серьёзную базу знаний:

  • опыт реальных проектов;

  • наши статьи о Flutter, его особенностях и проектах на нем;

  • методология обучения стажёров;

  • open source библиотеки и наработки, которые мы выкладываем в публичном репозитории на Github.

И эти знания должны работать и приносить пользу не только профессиональному сообществу, но и тем, кто только хочет освоить технологию. Так появилась идея создать свой курс по Flutter.

Рынку нужны практики, поэтому основная задача курса это помочь студентам получить реальные знания, познакомиться с наработанными best practice, быстро войти в тему и перейти к реальным задачам.


Мы не онлайн-школа и не стремимся тиражировать проекты. Мы разрабатываем мобильные приложения, готовы делиться практическим опытом и нам, как и рынку в целом, нужны компетентные специалисты.

Подробно о курсе можно почитать в статье.


Первый поток стартовал 3 месяца назад. За это время наши студенты не только прокачали свои практические навыки так, что скоро смогут стать Flutter-разработчиками на реальных проектах, но и здорово помогли в развитии курса. Их обратная связь, комментарии и советы стали бустом, который вывел курс на новый уровень.

И за это, ребята, большое вам спасибо!

5 марта стартует новый поток курса, и обновленная программа поможет будущим Flutter-разработчикам еще эффективней отработать практические навыки и освоить технологию.

Но давайте обо всем по порядку.

Можно ли изучить новую технологию самому, или почему практика важна?

Предположим, есть условный Егор. Он разработчик. Видит будущее за мобильными приложениями и хочет освоить новый стек. Ага, Flutter удобный и современный фреймворк для разработки приложений как под IOs, так и под Android, надо попробовать, - думает Егор.

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

И зачем тогда Егору платный курс? Тут все давно сказано за нас. Вспомним правило 10 000 часов, которое сформулировал психолог Андерс Эриксон чтобы добиться высокого уровня мастерства в своём деле, нужно посвятить практике не менее 10 000 часов. А ещё есть модель 70:20:10 Чарльза Дженнингса, в которой говорится о 70% практики, 20% - работы с наставником и 10% теории, которые необходимы для успешного освоения материала.

Документация и бесплатные курсы дадут Егору те самые 10%. Но одной теории мало для освоения технологии. Рынку нужны опытные разработчики, а не теоретики. И тут перед Егором встает резонный вопрос, как и где получить этот опыт. Отработать на практике под руководством ментора один из наиболее продуктивных вариантов. Именно такой формат мы предлагаем в своем курсе по Flutter.

Егор получит знания из первых рук. И это будут не просто лекции, а экспертиза и лучшие практики разработки из реальных проектов Surf на Flutter, которые дают опытные наставники. Обратная связь, код-ревью, рекомендации по конкретному проекту, как сделать лучше и исправить ошибку этого Егор не найдёт в документации. Но это точно пригодится ему в дальнейших проектах.

Вот что пишут студенты первого потока:

Основная ценность курса взаимодействие с наставниками. Тут дело не в сухой информации, которая и так есть в прекрасной документации flutter.

Этот курс отличная возможность глубоко разобраться в теме и понять, как устроена профессиональная разработка на flutter.

Домашние задания сдаются пулл-реквестами. Проверяют их по-взрослому от соответствия макету в figma до стиля кода. Смотрят код внимательно, замечают разные сомнительные архитектурные решения, проблемы с производительностью, подсказывают, как сделать лучше. Причем проверяют разные специалисты из команды surf.

Кирилл

Большой упор на практику, код ревью от тимлидов surf, и это очень круто. В каждой домашке отписывают более правильный код-стайл, лучшие подходы, отучают от го@нокода и всё в таком духе. Действительно стремятся сделать из вас хорошего разработчика!

Влас

Программа и для джуна, и для тимлида а так бывает?

На первый поток курса пришли ребята с очень разным уровнем подготовки в сфере разработки. Это были:

  • начинающие разработчики;

  • тимлиды;

  • senior-разработчики крупных команд;

  • архитектор Frontend-разработки крупного банка.

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

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

Решить эту проблему нам помогли 2 механизма. Первый индивидуальный выбор уровня нагрузки. Студент может проходить курс с любой скоростью, делать объём, который ему комфортно по уровню знаний или ориентируясь на уровень занятости на работе.

А еще студенты отметили, что гораздо удобней, когда загрузка на курсе равномерная, практические задания примерно одного объема и уровня сложности. Поэтому по обратной связи от ребят за 3 месяца мы переработали более 20% курса. Простые задания дополнили, а слишком сложные переформулировали или разделили на части.

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

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

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

  • Архитектура мобильных приложений в деталях (устройство под капотом, границы применимости различных моделей архитектуры)

  • Тестирование Flutter приложений (unit-тестирование, автотесты)

  • Взаимодействие с нативным приложением

  • Обзор возможностей Flutter Web (чем отличается от нативных приложений, JS/HTML под капотом, безопасность веб-приложений, какие есть возможности и производительность, как работать с поисковой оптимизацией и индексацией, как подготовить к использованию в e-commerce)

  • Основы языка Kotlin

  • Основы языка Swift

  • Обзор возможностей Navigator 2.0

  • Обзор возможностей Flutter Desktop

  • Взаимодействие с платформой (Advanced)

  • DevTools Profiling (Advanced)

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

Спикеры мастер-классов это опытные практики, эксперты в области разработки. Например, одним из выступающих стал Михаил Зотьев, который рассказал про внутреннее устройство и архитектурные особенности Flutter. А на мастер-классе по Flutter Web студенты разбирали и задавали вопросы про тонкости и ограничения применения Flutter в вебе, которые, в принципе, известны мало кому в индустрии.

Главный вопрос

Обычно со стороны студентов он звучит так а с трудоустройством поможете?

Есть 2 возможных направления, в которых курс будет вам полезен.

Наполните портфолио

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

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

Вы создадите мобильное приложение со списком интересных мест и достопримечательностей для путешествий по миру. Кейс максимально близок к реальным условиям работы в коммерческом проекте. Приложение состоит из 8 экранов. Всё по-взрослому описание требований от заказчика, дизайн-макет в Figma, серверная документация в Swagger.

Примеры экранов из практического кейса курса Flutter от Surf мобильное приложение со списком интересных мест и достопримечательностей для путешествий по мируПримеры экранов из практического кейса курса Flutter от Surf мобильное приложение со списком интересных мест и достопримечательностей для путешествий по миру

Разработку приложения вы ведёте в собственном репозитории. По мере продвижения по курсу проводите итерационный рефакторинг кода. Преподаватель делает ревью кода, контролирует, как вы используете EffectiveDart и best practice.

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

Что особенно понравилось и мне кажется очень важным, что идёт разработка приложения с нуля и последовательно по шагам. Особенно прикольно, когда что-то сделаешь, а потом на следующем задании надо всё переделать, потому что узнал новое. То есть это никак не разработка в стиле Лего по шагам к заданной цели. Тут по многу раз всё переделать можно. Как на реальном проекте, когда опыта ещё не набрался.

Виктор

Второе направление карьерные консультации

Вы составите резюме под руководством опытного HR, который специализируется на поиске и найме разработчиков, знает, на что обращают внимание работодатели. Получите рекомендации по поиску работы и консультацию от HR.

Лучшие студенты курса могут пойти на оплачиваемую стажировку в Surf. При е успешном прохождении вы сможете стать частью Surf Flutter team.

На языке цифр

По данным, собранным Кейт Джордан, исследовательницей в сфере образования и технологий, в среднем массовые открытые онлайн-курсы (MOOC) завершают около 15% поступивших.

По нашей статистике более 60% наших студентов работают над курсом ежедневно. Часть из них уже приближается к финальным этапам, а это показатель полезности программы для студентов.


Завершим отзывом студентки:

Про флаттер я узнала случайно, просматривая статьи Хабра и vc. Заинтересовалась, начала искать другие материалы и видео. Данная технология мне показалась очень привлекательной, поэтому следующим шагом была покупка курсов по Flutter и Dart на udemy.

Как только стартовал набор на курс у Surf, записалась сразу не раздумывая, так как компания является экспертом в этой области.

На мой взгляд, курс рассчитан не для новичков в программировании. Хорошо, что у меня есть небольшой бэкграунд + прошла предварительно купленные курсы. Отличием курса Surf от других является то, что у тебя постоянно работает мозг, как решить задачу, и никто, кроме тебя, её не решит и не покажут, как. Могут дать наводку, в какую сторону смотреть.

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

Курс для тех, кто не ленится и не опускает руки, когда что-то не получается. А опытные наставники поправят и направят в нужном направлении.

Татьяна


5 марта стартует новый поток курса по Flutter от команды Surf. Если хотите присоединиться и освоить разработку на Flutter на практике.

Регистрируйтесь на сайте

Подробнее..

Перевод Как выбрать мобильную кросс-платформу в 2021 году

25.02.2021 20:10:38 | Автор: admin

Кросс-платформенные решения - тренд в мобильной разработке. Уже есть различные технологии от PWA до Flutter и Kotlin Multiplatform. Как выбрать среди них?

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

Познакомимся с Женей

Евгения солюшн-архитектор. Она должна решить, как построить новое мобильное приложение для изучения английского языка не носителями: людьми из Турции, Италии или России. Давайте посмотрим, как Женя подходит к этой задаче.

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

В компании владельце приложения также ожидают, что разработкой будет заниматься единая команда как для Android- и iOS-приложений, чтобы свести к минимуму усилия по передаче знаний и максимизировать скорость команды. В будущем также планируют запустить веб-приложения. А еще в компании хотели бы упростить будущий найм.

Прогрессивные веб-приложения

Женя начинает свои исследования. Она гуглит мобильные веб-приложения и находит статью. В ней упоминаются Прогрессивные веб-приложения (PWA). Что это такое?

Прогрессивные веб-приложения это, по сути, веб-сайты, которые используют специальные API для доступа к определенным возможностям устройства. Эти API позволяют получить доступ к памяти на устройстве, интегрируются с Push Notifications (на Android) и, что самое важное, работать в отдельной вкладке браузера. Еще их можно установить на устройство иконкой, как настоящее приложение. Звучит неплохо! Давайте посмотрим на плюсы и минусы PWA:

Плюсы:

  • Всем занимается одна команда.

  • Достаточно навыков веб-разработки.

  • Легко обслуживать.

  • Все работает сразу из коробки.

Минусы:

  • Нет пушей на iOS.

  • Неудобный UX.

  • Ограниченная поддержка.

  • Трудно найти пользователи скачивают приложение в сторах.

А вот и хорошая табличка, показывающая доступность PWA в разных браузерах:

Гибридные приложения

Жене не нравится, что PWA обладают ограниченной поддержкой пушей. Поэтому она гуглит дальше.

Она обнаруживает гибридный подход: давайте поместим наш сайт в WebView в реальном нативном приложении. Оказывается, можно поместить свой мобильный сайт со всем его HTML, CSS и JS-кодом в ресурсы приложения. Затем вы помещаете WebView в корневой ViewController/Activity приложения и указываете на эти ресурсы.

Пользователи смогут найти приложение в Google Play и AppStore, чего не было в случае с PWA. А еще, если вам нужны дополнительные возможности, вы можете легко добавить их ведь у вас настоящее приложение, для этого уже есть необходимые фреймворки.

Плюсы:

  • Высокая скорость работы.

  • Настоящая кросс-платформенность.

  • Можно пользоваться практически всеми преимуществами нативных приложений.

Минусы:

  • UX не очень (ведь перед нами по-прежнему сайт в шкуре приложения).

  • Небольшое сообщество разработчиков.

Архитектура гибридного приложенияАрхитектура гибридного приложения

Примеры таких приложений: Appcelerator, Ionic, Apache Cordova.

Нативные кросс-платформенные приложения

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

Евгения все еще пытается получить единую кодовую базу для своего решения, но при этом решить проблему с вялым UX. Поэтому она читает больше про нативные кросс-платформенные фреймворки.

Xamarin

Xamarin это решение на основе .NET от Microsoft.

Xamarin (или NET 5.0) помогает кросс-платформенное приложение на C# и .NET в мобильном приложении. Xamarin Forms (или MAUI) это библиотека для построения пользовательского интерфейса в таких приложениях. Теперь они оба переименованы:

Плюсы:

  • Легко разрабатывать в компаниях с сильной экспертизой в технологиях Microsoft.

  • Полностью кросс-платформенное.

  • Потенциально можно сделать его и веб-приложением.

Минусы:

  • 2 виртуальные машины в приложении на Android - JVM и Mono

  • Мало инструментов и библиотек

  • Долгое время исправления багов в Xamarin

  • Растет размер бандлов

React Native

React Native создан для того, чтобы привнести React в мобильные технологии. И это действительно хорошо сработало! Согласно статистике Bitrise, React Native занимает 13% рынка. Его также используют более 500 тысяч репозиториев на Гитхабе.

React Native использует языки Javascript или TypeScript для разработки приложений и транслирует компоненты React Native в компоненты платформы. Так, превращается в TextView на Android.

Приложение включает в себя JavaScript VM для выполнения JS-кода логики и пользовательского интерфейса приложения. React Native также умеет работать с API платформы. С такой архитектурой можно создать любое приложение, даже мобильную игру.

Плюсы:

  • Большое сообщество.

  • Пользуются в Skype, Discord, Shopify, Tesla, Artsy.

  • Доступны нативные возможности приложений.

  • Можно писать на React.

Минусы:

  • Долгое время запуска.

  • Экстеншены и работа в фоне требует написания нативного кода.

  • Проходит ре-архитектуризацию.

Flutter

Flutter это кросс-платформенный фреймворк с открытым исходным кодом от Google, основанный на языке Dart. В отличие от React Native, Flutter использует свой собственный 2D-движок для отрисовки пользовательского интерфейса в нативном виде. Такой подход обеспечивает независимость от версии ОС, на которой работает приложение. Flutter приложение также компилирует Dart в нативный для платформы код в релизной сборке, таким образом в рантайме Dart VM становится не нужна. Советую нашей Жене мое подробное сравнение Flutter и React Native.

Flutter привлек к себе большое внимание, собрал более 100 000 звезд, а количество приложений, собранных с его помощью, продолжает стремительно расти. Согласно той же статистике Bitrise, уже 9% билдов в прошлом году собраны на Flutter. Это очень много для такой молодой технологии.

Плюсы:

  • Хорошая производительность

  • Декларативный UI

  • Поддерживает веб

Минусы:

  • Ненативный UI

  • Малопопулярный язык Dart.

  • Недостаток некоторых инструментов (например, в плане контроля безопасности).

Kotlin Multiplatform

Xamarin, Flutter и React Native позволяют вам написать практически весь код единожды и запускать его и на iOS, и на Android.

Kotlin Multiplatform делает иначе. KMP считает, что пользовательский интерфейс сильно зависит от платформы и самого устройства (скажем, планшета или веб-сайта). Однако бизнес-логика остается практически неизменной. Почему бы не переиспользовать прежде всего ее?

Итак, с KMP у вас все еще есть два нативных приложения, которые пользуются одной бизнес-логикой. Вы можете использовать все, что угодно в пользовательском интерфейсе: будь то родной Android Views, JetPack Compose или Swift UI для iOS. Вы даже можете использовать Flutter или React Native для вашего пользовательского интерфейса! Он все равно будет прекрасно работать с Kotlin Multiplatform. Вот несколько примеров.

Плюсы:

  • Родной язык для разработчиков Android.

  • iOS разработчикам нетрудно читать Kotlin код.

  • Единая среда разработки для Android и iOS.

  • Подходит для реализации веб-приложений.

Минусы:

  • Молодое решение, все еще в альфа-версии.

  • Нельзя переиспользовать UI.

Что же выбрать?

Итак, наша Женя собирается выбрать свое решение. Вот что мы ей посоветуем:

  • Согласна на мобильный сайт вместо приложения, несмотря на ограничения в UI? Тогда PWA.

  • В компании все пишут на .NET? Значит, Xamarin.

  • Основной стек это JS/TypeScript? Выбираем React Native.

  • Будем шарить между приложениями не UI, а только бизнес-логику? Значит, Kotlin Multiplatform.

Конечно, это не все критерии: например, мы не рассмотрели доступность разработчиков на рынке или темы поддержки новых версий OS. Но основной алгоритм - выше :)

Удачи, Женя!

Подробнее..

Не мешай ему взрослеть как оптимизировать растущее приложение, чтобы оно оставалось удобным для пользователя

27.02.2021 14:17:14 | Автор: admin
Количество установок приложения IntellectoKids Classroom & Learning games.Количество установок приложения IntellectoKids Classroom & Learning games.

Привет, Хабр! Меня зовут Андрей Романенков, я работаю ведущим программистом в IntellectoKids. Мы создаем образовательные приложения для дошкольников.

Этой статьи могло бы не быть, ведь детские мобильные игры, как правило, имеют простую логику, не требуют сложного кода и могут функционировать без дополнительных усилий со стороны разработчиков.

Но есть одно но.

В определенный момент ваше простенькое приложение может превратиться в перспективный проект с десятками мини-игр внутри и еженедельными обновлениями. Собственно, это произошло с нами. И тут начинается самое интересное. Представьте, что геймдизайнеры постоянно добавляют в игры новые уровни, художники новые текстуры, а локализаторы локализацию на волапюк. Репозиторий растет, приложение раздувается. Как результат увеличивается время скачивания. А это уже может повлиять на популярность продукта и на его продвижение.

Как решить проблему? За четыре года мы наработали опыт, не претендующий на абсолютность или даже оригинальность, но которым я хочу поделиться. Возможно, вам будет полезно.

Саб-модульность, многорепозиторность, подход к общему коду

Всего у IntellectoKids 4 приложения. Поскольку сервисы у них идентичны (например, логика работы с сервером, аналитика, покупки) и много одинакового кода, мы выделили общий функционал в отдельный репозиторий, который каждый конкретный проект подключает через git submodules. Выскажу довольно очевидную мысль, что когда ваши проекты с общим кодом множатся, код нужно скорее выделить в единую библиотеку. Вроде бы все это понимают, но часто откладывают на потом, а чем дальше вы откладываете, тем тяжелее будет процесс слияния.Помимо выделения репозитория для общих сервисов, мы создали также другой общий репозиторий для более базовой библиотеки утилит и вспомогательных классов.Второй вариант подключения дополнительного функционала появился у нас с добавлением Package Manager в Unity. Так можно делать, когда ваша библиотека устоялась и если вам необходимо подключить какую-то стороннюю библиотеку с репозитория как package.Когда проект длится давно, а количество контента увеличивается с каждым днем, то из-за раздувшейся истории и обилия больших файлов рано или поздно вы столкнетесь с проблемой размера репозитория. У нас текущий репозиторий перевалил за 10Гб (сейчас 14 гб), с чем справляются не все хостинги (большая часть из них ограничивает размеры хранилища).В борьбе за производительность нам помогают чистка истории и использование git lfs, а также внимательное отношение к размеру и формату импортируемых в проект ассетов. Например, импортирование mp3 и ogg файлов вместо wav; и отсекание слишком больших текстур.

Локализация, в том числе RTL-языки

Наши приложения локализованы более чем на 40 языков, включая RTL языки (предполагающие чтение справа налево). Система локализации самописная, но в целом она похожа на типовые решения из Asset Store (такие, как I2 Localization). В Google-таблицах хранятся ключи и значения. Есть базовая таблица для всех игр, и дополнительные таблицы для каждой конкретной игры. Каждая таблица в Google-документах скриптами собирается из других вспомогательных таблиц, которые редактируют локализаторы.

Данные из вспомогательных таблиц(цветные закладки внизу) попадают в финальные таблицы локализаций.Данные из вспомогательных таблиц(цветные закладки внизу) попадают в финальные таблицы локализаций.

На клиенте наши скрипты MonoBehaviour выцепляют нужные значения и выставляют их в TextMesh Pro компоненты. Клиент может обновлять таблицы как в режиме редактора, так и рантайма. У клиента таблицы хранятся в csv формате, и в память грузится только нужный язык, так как количество ключей для каждого языка превышает уже пять сотен!

Для поддержки арабского языка и иврита мы реализовали ряд компонентов, осуществляющие превращение верстки под обычные языки: отражение, переставление якорей, смена последовательности элементов к группе, а также компонент для полной поддержки возможностей по форматирования текста в TextMeshPro.

Пример стандартной вёрстки на английском.Пример стандартной вёрстки на английском.То же окно, но на иврите.То же окно, но на иврите.

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

Когда волна больше определённого значения у Animator кролика включается параметр IsTalking.Когда волна больше определённого значения у Animator кролика включается параметр IsTalking.

Бандловость: 2 подхода. Эволюция в работе с бандлами

По мере развития проекта (добавления новых уровней и другого контента, увеличения количества локализаций) постоянно рос общий размер содержимого. С самого начала нам было понятно, что необходимо использовать бандлы, иначе размер клиента был бы огромен и сейчас составлял бы 3 ГБ. Да, не Modern Warfare, но всё же для еженедельной скачки это неприемлемо.

В какой-то момент мы, правда, провели эксперимент с выпуском таких больших релизов (тогда размер был примерно под два гигабайта), но это сразу заметно отразилось на общей статистике приложения. Сейчас у нас зашиты в билд только небольшие бандлы, необходимые для ускорения старта. В нашем основном приложении IntellectoKids Classroom&Learning Games больше тысячи бандлов общим размером 2.5 гигабайта. Может показаться, что это слишком много, но если умножить количество встроенных игр на количество языков, и добавить к этому, что у каждой игры есть множество уровней с насыщенным контентом, то всё сразу станет понятно.Из-за особенности геймплея каждая игра имеет свои нюансы объединения ресурсов в бандлы. Где-то можно поместить все локализованные фразы в один бандл, так как их общий размер мал, а где-то необходимо разделить и поместить каждый язык в отдельный бандл. В каких-то играх несколько уровней объединены в один бандл, а в других должен быть бандл у каждого уровня. При формировании бандлов мы создаём manifest файл, описывающий имена и хэши бандлов.

Загрузчик при обновлении версии качает этот manifest и проверяет, какие бандлы нужно закачать заново, а какие удалить. Изначально игра поддерживала фоновую загрузку бандлов во время игры, но позже мы отказались от такого подхода, так как он таил в себе много скрытых проблем (сценарии обновления бандла во время использования его игрой, баги Unity в выгрузке бандлов и т.п.) Сейчас мы перешли на более классический вариант, когда игроку в нужные моменты показывается экран загрузки бандлов.Самый маленький большой нюанс нашего проекта, это, безусловно, релиз и деплой более чем тысячи бандлов. На первоначальном этапе, когда бандлов было меньше, мы размещали их в Google репозитории и даже использовали веб-интерфейс Chrome для их загрузки, но довольно быстро перешли на собственный билдер-загрузчик, выполненный в виде инструментария в Unity. Он позволяет загрузить нужные бандлы, задать версионность билда и настроить другую рутину.

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

Первоначально бандлы лежали в Google Cloud Storage, затем мы перешли на Amazon Web Services. Основная статья затрат у бандлов это скачивание. С переходом на AWS и CloudFront нам удалось оптимизировать издержки. Хоть это, а также переход на новый API с доработкой инструментов деплоя занял некоторое время, но оно того стоило.

Переход на новые версии Unity

За прошедшие четыре года разработки мы многократно обновляли версии Unity. В целом мы стремимся делать это часто, но ведем разработку только в LTS версиях из-за их большей стабильности.

Если вы растите приложение, будьте готовы, что требования к нему будут довольно часто меняться. Нам много раз приходилось перерабатывать фреймворки аналитики (один Facebook включался и выключался больше пяти раз).

(Чтобы подчеркнуть всю глубину глубин этой ситуации я хотел вставить картинку описывающюю определённую эмоцию, но не нашёл подходящей картинки человека схватившегося за голову или попу.)

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

Что можно добавить к сказанному в статье?

Пожалуй, стоит понимать, что ваш проект всегда будет далек от того идеала, который вы себе намечтали. Но это не значит, что нужно откладывать очевидные улучшения архитектуры. Особенно в условиях, когда требования к функционалу приложения постоянно меняются. Ведь неделька на рефакторинг может сэкономить вам месяцы мучительной разработки, но помните, что "Real artists ship" и если ваш рефакторинг слишком большой, а бизнес не стоит на месте, то и рефакторинг может подождать. Тут дам совет, особенно скромным программистам: более четко доносить до менеджмента такие критичные вещи, чтобы ваш технический долг не рос.

Dixi

Подробнее..

Avito Android meetup работа с Gradle и проблемы при сборке проектов

01.03.2021 12:17:20 | Автор: admin

Привет, Хабр! 11 марта в 18:00 по Москве мы проведём онлайн-митап для андроид-разработчиков.

В этот раз без внешних спикеров все доклады будут от инженеров нашей платформенной команды Speed, которые отвечают за быструю доставку изменений во всех андроид-приложениях Авито до пользователей. Ребята каждый день решают задачи, связанные с CI/CD и локальной работой с проектами, так что им есть, чем поделиться.

Доклады

Как правильно писать на Gradle в 2021 Дмитрий Воронин

В Gradle есть куча способов решить одну и ту же задачу, однако часть из них оптимальнее других. Я разберу несколько распространённых ошибок и покажу, как сделать лучше.

Как защищаться от частых проблем при сборке проекта Евгений Кривобоков

На сборку проекта влияет много неочевидных настроек. Они могут привести к медленной сборке или даже ошибкам в рантайме. Как защититься от неправильной настройки проекта? Что делать, если соглашения не работают или не получается написать Android lint проверку?

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

Gradle build scan на коленке Сергей Боиштян

Мы сделали свой Gradle-плагин с генерацией HTML-странички с полезной информацией, чтобы наши разработчики не тратили время на поиск ошибок в CI-сборках. Я расскажу об этом инструменте от зарождения идеи до реализации и том, как подключить его в свой проект.

Доклад будет интересен тем, кто страдает от поиска ошибок в своём CI и developer experience инженерам, которые могут переиспользовать наше решение или идеи.

Когда закончим с докладами, можно будет неформально пообщаться в Зуме со спикерами. Ссылка появится в чате трансляции.

Пароли и явки

Трансляция на нашем ютуб-канале стартует в четверг 11 марта в 18:00 по московскому времени. На трансляции можно сразу нажать кнопку напомнить, чтобы ничего не пропустить.

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

До встречи в онлайне!

Подробнее..

XCResult как и зачем читать

04.03.2021 16:23:55 | Автор: admin


В 2018 году Apple в очередной (третий) раз обновили формат, в котором выдаётся информация о прогоне тестов. Если раньше это был plist файл, который представлял из себя большой xml, то теперь это большой файл с расширением xcresult, который открывается через Xcode и содержит в себе кучу полезной информации, начиная c результатов тестов с логами, скриншотами и заканчивая покрытием таргетов, диагностической информацией о сборке и многим другим. Большинство разработчиков не работает каждый день с этим, но инфраструктурщики в данной статье могут найти что-то полезное.

Разложим по полочкам плюсы и минусы обновления формата


В чем минусы обновления формата?
Много весит, а это значит обмен такими файлами с CI сервером может оказаться долгим.
Если нет Xcode, то его не открыть (сомнительно, что у тестировщика или разработчика не будет Xcode, но все же).
Возможная поломка существующих инструментов интеграции. Снова учиться работать с чем-то новым.

Чем удобен новый xcresult?
Открывается нативными средствами через Xcode.
Можно передавать коллегам из QA и разработки, даже если у них нет локально проекта. Все откроется и покажет нужную информацию.
Содержит исчерпывающую информацию о прогоне тестов.
Можно читать не только через Xcode.

Вот о последнем пункте мы и будем говорить в этой статье.

Зачем читать XCResult не через Xcode?


Если у вас в компании настроены процессы CI&CD, то наверняка вы собираете метрики по сборкам проекта, по стабильности и количеству тестов, и, конечно, данные по тестовому покрытию. Скорее всего, где-нибудь на Bamboo, Jenkins, Github у вас рисуются упавшие тесты или статус CI, или процент покрытия. Такие операции принято автоматизировать и отдавать на откуп бездушным машинам. Какие инструменты есть у нас для этого?
Apple, вместе с релизом нового формата, выпустили и инструменты xcresulttool и xccov, с которыми можно работать из терминала.

Что мы можем достать, используя xccov?


xcrun xccov view --report --json /path/to/your/TestScheme.xcresult

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



Помимо протоколов выделим следующие структуры: CoverageReport агрегатор всего и корень. Он содержит в себе массив объектов Target. Каждый Target содержит в себе массив File, которые, в свою очередь, содержат массив Function. Эти объекты будут реализовывать протоколы, которые описаны выше.
Нас интересует поле lineCoverage. Для составления красивого отчета (как в fastlane) обратимся к полю lineCoverage и пройдем по всем объектам нехитрой функцией:



Получим что-то похожее на:

Coverage Report Summary:

Utils.framework: 51,04 %

NavigationAssistantKit.framework: 0,0 %

NavigationKit.framework: 35,85 %

Logger.framework: 20,32 %

FTCCardData.framework: 78,21 %

FTCFeeSDK.framework: 25,25 %

ErrorPresenter.framework: 2,8 %

MTUIKit.framework: 0,24 %

AnalyticsKit.framework: 47,52 %

EdaSDK.framework: 1,18 %

Alerts.framework: 85,19 %

Resources.framework: 39,16 %

QpayApiTests.xctest: 88,37 %

FTCFeeSDKTests.xctest: 97,91 %


P.S. Для того, чтобы coverage собирался, необходимо добавить в вашу команду тестирования параметр -enableCodeCoverage YES или включить в настройках схемы в Xcode.

Какие возможности даст xcresulttool?


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

Для начала неплохо ознакомиться с самим интерфейсом:

xcrun xcresulttool --help

OVERVIEW: Xcode Result Bundle Tool (version 16015)

USAGE: xcresulttool subcommand [options] ...

SUBCOMMANDS:

export Export File or Directory from Result Bundle

formatDescription Result Bundle Format Description

get Get Result Bundle Object

graph Print Result Bundle Object Graph

merge Merge Result Bundles

metadata Result Bundle Metadata

version XCResultKit Version


Чтобы прочитать структуру, нам достаточно вызвать команду:

xcrun xcresulttool get --path /path/to/your/res.xcresult --format json

Здесь мы получим оглавление для нашего xcresult бандла. Что собиралось, какие тесты прогонялись, сколько времени заняло, где лежат скриншоты и логи, и какие были предупреждения компилятора. Для нас главное достать идентификаторы файлов, в которых лежит информация о тестах.

xcrun xcresulttool get --path /path/to/your/res.xcresult --format json --id {id}

Тогда мы получим объекты с тест-таргетами, типом тестов, которые разбиты по тест-классам и test suits с отчётами с логами, скриншотами, временем выполнения и прочей информацией по каждому тесту.
К сожалению, причину падения красных тестов не получится вытащить просто для этого придётся делать ещё один запрос на каждый упавший тест (а на самом деле даже не один! Если тест крэшнул, то крэшлоги вместе со стректрейсом лежат в другом месте и это ещё один запрос!

Для Failure Summary используется тот же запрос:

xcrun xcresulttool get --path /path/to/your/res.xcresult --format json --id {id}

А вот для крэшлогов нужно убрать --format json из запроса, т.к. там просто строка и при передаче форматтера инструмент выдаст ошибку.

Что делать с этими справочными знаниями дальше?


Автоматизировать, конечно же! Если вы попробуете выполнить эти команды, то увидите, что ответы гигантские и их тяжело читать. Как автоматизировать? Ruby, Python Или Swift?
Конечно же, swift. Его знает любой современный iOS разработчик. Проект открывается в Xcode, доступна отладка, подсветка синтаксиса, строгая типизация. Короче, мечта! Особенно при появлении Swift package manager.
Ни для кого не секрет, что с помощью swift мы легко можем запускать процессы, слушать ошибки и получать выходные данные. В самом простом случае мы можем обойтись такой конструкцией:



Нам остается теперь только исследовать формат XCResult через уже знакомые нам xcrun xcov и xcrun xcresulttool. Например, чтобы прочитать покрытие тестами, мы используем:



А чтоб получить оглавление XCResult нам нужно выполнить:



Но как нам получить наши заветные структуры CoverageReport и XCResult?
Получаем строку из Data, которую вернет нам первая Shell команда и помещаем содержимое сюда: quicktype.io.
Сервис сгенерирует нам что-то похожее на нужные свифтовые структуры. Правда использовать результат как есть не получится. Придётся пристальнее изучать структуру ответа и выбрасывать дубли. Тем не менее такая работа не составляет большого труда. Можно отбрасывать ненужные части, а можно заняться исследованием и выделить несколько основных кирпичиков:



На основании этого описать уже остальные структуры, например:



или даже такие сведения о компьютерах, на которых совершался прогон:



Ну а этим-то как пользоваться?


Есть два пути, как пользоваться нашим скраппером. Первый как executable, и здесь здорово помогает библиотека swift-argument-parser от Apple. До этого приходилось писать обработку аргументов самим, покрывать тестами, поддерживать. Сейчас эту работу взяла на себя популярная библиотека, меинтейнерам которой можно доверять.
Есть две команды: получить отчёт по покрытию тестами и сгенерировать junit отчёт о результатах тестирования. Нужно сбилдить проект и запускать бинарник, передавая необходимые аргументы:



Второй путь использовать этот проект как библиотеку. У нас есть большой CI проект, который отвечает за сборку, тестирование и доставку нашего продукта KoronaPay. Например, мы можем по результатам прохождения тестов извлекать все assertion failures и крэши в тестах приблизительно так:



Или получать красные тесты, анализировать флаки и перезапускать только их.
А как анализировать? Всё просто и непросто одновременно. Чтобы достать детали причины падения теста, надо сделать дополнительный запрос к xcresult по идентификатору failure summary. А затем из failure summary вытаскивать информацию. На сегодняшний момент мы научились искать крэши в тестах и lost connection случаи, а также вытаскивать причины. Понять, что произошел крэш несложно. Надо лишь найти в failureSummaries заветные слова crashed in.



Чуть сложнее вытащить причину крэша.
Здесь нам пригодится механизм рефлексии в swift, который хоть и несколько ограничен, но отлично подходит для решения этой задачи. Необходимо найти все объекты типа Attachment с именем kXCTAttachmentLegacyDiagnosticReportData.



В методе reflectProperties нет ничего магического, это простенький extension для Mirror:



Еще одна категория красных тестов ассерты. В отличие от крэшей здесь не получится просто поискать строку crashed in. Такие тесты могут маскироваться под lost connection случаи. Чтобы докопаться до причины, придется пройтись по нескольким массивам внутри объекта TestCase примерно так:



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



Вместо заключения


Как и все существующие в этой области решения, наш скраппер не является исчерпывающим инструментом для анализа xcresult. Чтобы получить всю информацию и посмотреть скриншоты, все еще надо открывать xcresult через Xcode. Однако если у вас настроен CI и вы хотите видеть результаты тестов быстро, то, скорее всего, сможете оценить связку junit и нашего xcscrapper по достоинству.
Подробнее..

7 идей, как использовать AR-технологии в музее

04.03.2021 16:23:55 | Автор: admin

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

Рассмотрим 7 вариантов применения дополненной реальности на примерах музеев и художественных галереей по всему миру. После прочтения вы сможете решить, какая идея подходит именно вам.

1. Дополните коллекцию известными произведениями искусства

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

С появлением цифровых технологий это больше не является проблемой: дополненная реальность способна воплотить в жизнь любые идеи даже с ограниченным бюджетом.

В качестве примера рассмотрим проект Латвийского национального художественного музея. В его основе лежит идея воплощения произведений искусства известного латвийского коллекционера в художественной галерее с использованием цифровых технологий. Благодаря AR каждая картина была преобразована в цифровой формат.

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

Ещё один вариант: сотрудничество с художниками для создания эксклюзивной AR-выставки, которая дополнит уже имеющуюся коллекцию. В настоящее время многие художники занимаются цифровым искусством и создают собственные приложения, которые также можно включить в музейную коллекцию. Например, автор приложения SAN.app, используя технологию GPS, размещает свои работы по всему миру.

Таким образом, можно обратиться к талантливым цифровым художникам, которые будут проводить различные AR-выставки в вашем музее.

2. Дайте посетителям возможность самостоятельно познакомиться с коллекцией

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

Дополненная реальность дает возможность знакомиться с произведениями искусства в своем собственном темпе. Используя AR-приложения, посетители детальнее изучают интересующие их выставки и произведения в удобное для них время и порядке.

3. Повышайте качество экскурсий с помощью AR-технологии

Речь идет не о замене людей технологиями, а скорее о положительном влиянии технологий на ход экскурсии.

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

4. Сделайте ваш музей лучшим в своем роде

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

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

5. Создайте виртуальный AR-портал к музею

В качестве иллюстрации можно рассмотреть проект the Sunshine Aquarium в Токио. Его суть заключается в следующем: путь к аквариуму оснащен системой дополненной реальности. Туристы не просто посещают аквариум, а получают незабываемые впечатления от AR-путешествия еще на подходе к нему.

На этом пути туристы встречают пингвинов, созданных с помощью AR-технологии и GPS. На протяжении всего маршрута интерактивные животные выступают в качестве гидов.

Создание AR-приложений на основе GPS требует значительных издержек и тщательной работы над их продвижением и ознакомлением с ними посетителей. Это особенно актуально для мегаполисов, основные потоки посетителей в которых формируются благодаря туристам.

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

Следующий способ привлечения посетителей в музей размещение выставки или AR-портала в музей в общественном месте, популярном среди туристов. Чем популярнее место, тем больше людей узнает о музее. Посредством AR-технологий туристы могли бы детально ознакомиться с коллекцией музея и приобрести билеты на выставку. Захватывающий опыт работы с AR надолго запомнится и простимулирует людей прийти в музей.

6. Используйте AR-зеркала

Зеркала дополненной реальности это отличная платформа для музеев: посетителям не понадобятся гаджеты для получения информации, а значит, можно будет привлечь больше людей. С помощью AR-зеркал возможно организовать взаимодействие с экспонатами в самом музее и за его пределами. Показательным примером могут служить динозавры, воссозданные с помощью AR-технологий в Парке Юрского периода.

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

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

7. Создайте сувениры с AR-эффектом, чтобы посетители захотели вернуться

Обычно в конце экскурсии посетители могут купить копии или миниатюры знаменитых произведений искусства. Именно в них стоит интегрировать технологию дополненной реальности. Например, посредством AR можно передать дополнительную информацию о коллекции, к которой принадлежит экспонат, или подвести итог о посещении музея, показав короткое видео. Такие сувениры несут с собой дополнительную ценность.

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

Тем не менее, каждый музей, определившись со своими целями, должен самостоятельно решить, какую конкретную идею ему следует реализовать.

Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru