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

Разработка под android

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. Отдельная благодарность Звиаду Кардава и Джеффа Ван Дер Ступа за помощь в подготовке статьи.
Подробнее..

Системный гайд по созданию White Label android-приложений

07.02.2021 14:05:43 | Автор: admin

Как написать код один раз, а продать 20 мобильных приложений? Мы нашли ответ путём проб и факапов и разложили опыт по пунктам: из статьи вы узнаете, как безболезненно реализовать White Label android-проект.

Greetings and salutations! По работе я однажды получил крутую задачу по разработке White Label android-приложения. Изучил достижения коллег в этой области и нашёл только:

  • входные гайды (раз, два, три, etc) о механизмах, но без промышленного дизайна;

  • статьи, в которых освещены узкие аспекты задачи (раз, два, etc).

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

1 Ставим задачу

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

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

Бюджет ограничен... фичи типовые... да здравствует конструктор приложений! Или White Label продукт? Пока отложим термины и опишем задачу: генерировать приложения из единой кодовой базы, каждое с дизайном под бренд клиента и только нужными ему фичами.

Задача: создавать приложения для разных клиентов из единой кодовой базы

1.1 Визуализируем решение

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

У каждого бренда своя программа лояльности, например в SEPHORA накапливаются бонусные баллы и процент скидки, а в Пятёрочке только баллы. В приложениях это выглядит так:

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

Как реализовать такой проект без боли? Прочитайте статью и найдёте ответ.

1.2 Детализируем требования

Разложим видение по полочкам: как в ТЗ, но проще.

Функциональные требования

  1. Реализовать общие модули фичей:

    • новости клиент узнаёт об акциях и жизни сети магазинов;

    • лояльность получает дисконтную карту, узнаёт баланс, пробивает на кассе;

    • ...

  2. Задавать отдельно для каждого приложения:

    • наборы фичей, чтобы выбирать сами модули и настраивать их параметры;

    • бренд, чтобы настраивать цвета и менять ресурсы: шрифты, картинки, зашитый контент.

Нефункциональные

  • у приложений должен быть общий код;

  • настройка нового приложения меньше четырёх часов разработчика;

  • архитектура должна упрощать расширение модулей и поддержку от 10 до 100 приложений.

1.3 Что пилим то? Конструктор? White Label?

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

  1. Что даёт конструктор/платформа:

    • универсальный сервис сборки приложений из готовых компонентов;

    • например, AppGyver dragndrop вёрстка, программирование на низкоуровневых фичах (открыть экран, сделать фото);

    • творим что угодно от приложений по покупке золота до приёмки грузов.

  2. Что даёт White Label:

    • конструктор для конкретного типа приложений, например для такси;

    • ребрендинг под клиента и настройка высокоуровневых фич (новости, профиль)

Наш фокус на системах лояльности. Значит, делаем White Label. Гуглим white label android development и находим то, что нужно.

2 Проектируем и воплощаем

Строим системную схему White Label приложения

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

и получим четыре жирные проблемы:

  1. Как шарить кодовую базу между приложениями?

  2. Как сделать ребрендинг?

  3. Как задавать конфиги?

  4. Как отключать ненужные модули и настраивать необходимые?

В очередь, проблемы, в очередь!

2.1 Шарим код

Задача одна кодовая база, до 100 приложений. Решение Gradle Product Flavors.

Если вы ещё не знакомы с Gradle Product Flavors, советую почитать документацию или общие статьи. А можно и сразу в контексте White Label: кратко или в формате инструкции

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

Главное преимущество. Относительная простота переиспользования кода и ресурсов, удобство сборки.

Главный недостаток. Если вариантов больше 100, то в проекте и конфигах будет тяжело ориентироваться. Но у нас меньше, поэтому ок.

Альтернативы, на мой взгляд, рассматривать нет смысла: решение надёжное, из коробки.

Пример flavors. Допустим, на старте делаем два приложения:

  1. Лояка абстрактная компания;

  2. Ювелирия сеть ювелирных магазинов.

Назовём flavors соответственно loyaka и jewelry. Сразу реализуем best practice конфиг каждого flavor вынесем в отдельный файлик. Зачем? Станет ясно чуть позже.

Пока создадим:

  1. папку project_flavors;

  2. в ней gradle-скрипты flavor_loyaka.gradle, flavor_jewelry.gradle и flavors_common.gradle;

  3. задействуем скрипты в build.gradle уровня app.

Здесь и далее привожу сокращённые примеры из тестового проекта к статье.

flavor_loyaka.gradle

apply from: "$rootDir/project_flavors/flavors_common.gradle"  android {    productFlavors {        loyaka {            dimension APP_DIMENSION            resValue "string", APP_NAME_VAR, 'Лояка'            applicationId BASE_PACKAGE + 'loyaka'        }    }}

flavor_jewelry.gradle

apply from: "$rootDir/project_flavors/flavors_common.gradle"  android {    productFlavors {        jewerly {            dimension APP_DIMENSION            resValue "string", APP_NAME_VAR, 'Ювелирия'            applicationId BASE_PACKAGE + 'jewelry'        }    }}

flavors_common.gradle

android {    ext.DIMENSION_APP = "app"    ext.APP_NAME_VAR = "app_name"    ext.BASE_PACKAGE = "com.livetyping."}

Наконец, задействуем flavors в build.gradle уровня app:

...apply from: "$rootDir/project_flavors/flavor_loyaka.gradle"apply from: "$rootDir/project_flavors/flavor_jewelry.gradle"apply from: "$rootDir/project_flavors/flavors_common.gradle"android {    ...    flavorDimensions APP_DIMENSION}...

2.2 Перекрашиваем

2.2.1 Концепт

У каждого приложения свой бренд, который складывается из:

  • цветовой схемы;

  • шрифтов, картинок, строк;

  • зашитого контента (соглашений, ссылок в соц. сети).

Благодаря flavors тоже решим задачу просто. Загрузим в голову 3 факта:

  1. общие код и ресурсы проекта лежат в папке main;

  2. для gradle main это как дефолтный flavor;

  3. у каждого flavor свои исходники. Например, общие ресурсы лежат в main/res, а специфичные для флэйвора loyaka в loyaka/res;

Что произойдёт, если в main/res и loyaka/res будут картинки с одинаковым именем animal.webp? Возникнет конфликт, и чтобы решить его, Gradle переопределит базовые ресурсы кастомными. Если непонятно, поможет диаграмма:

Слева ресурсы по flavor; справа итоговый APK.Слева ресурсы по flavor; справа итоговый APK.

Задача решена! Уберём дефолтные ресурсы в main, а в конкретных flavor будем переопределять по необходимости.

2.2.2 Best practices

Крайне важно заранее договориться с дизайнерами:

  • ресурсы в приложениях называем одинаково вставляем в проект прямиком из дизайна;

  • тему задаём чётким набором цветов для перекрашивания копируем colors.xml в новый flavor и просто меняем значения.

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

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

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

2.2.3 Пример схемы цветов

Задаём цвета бренда в файле project_styleguide.xml:

<?xml version="1.0" encoding="utf-8"?><resources>    <color name="active">#68b881</color>    <color name="background">#36363f</color>    <color name="disabled">#daede0</color>    <color name="field_dark">#f5f5f5</color>    ...</resources
<?xml version="1.0" encoding="utf-8"?><resources>    <color name="active">#a160d5</color>    <color name="background">#f6ebff</color>    <color name="disabled">#e2c8f6</color>    <color name="field_dark">#f5f5f5</color>    ...</resources>

2.3 Задаём конфиг

2.3.1 Концепт

Фичи настраиваем на двух уровнях:

  1. отключаем ненужные модули;

  2. меняем параметры внутри самих модулей.

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

Упрощённый пример дока в формат модуль-фича-параметры:

  1. Подключаемые модули:

    • лояльность;

    • новости;

  2. Аутентификация:

    • логин пользователя: телефон или email;

    • маска логина.

  3. Карта лояльности:

    • тип штрих-кода: EAN-8, EAN-13, CODE-128.

2.3.2 Пути решения

Как сделать качественный конфиг? Само качество определим так:

  1. удобство работы простота чтения, простота заполнения (в идеале, хотим DSL);

  2. Скорость обработки важно, чтобы чтение конфига не тормозило приложение.

Выделим основные пути:

  1. Gradle buildConfigField

    • задаём переменные в gradle скрипте;

    • во время компиляции генерится java класс BuildConfig, переменные доступны как его поля.

  2. JSON

    • json объект в файле;

    • зашит локально, либо получаем с сервера.

Кратко оценим пути по критериям.

2.3.3 Путь 1. Gradle buildConfigField

Плюсы:

  • удобство создания делаем DSL на минималках: выносим типы и возможные значения параметров в переменные; выявляем синтаксические ошибки на компиляции;

  • простота большинству уже знаком;

  • скорость обращаемся к классу BuildConfig в памяти.

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

Пример переменной на условном DSL:

buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_SHOPS

2.3.4 Путь 2. JSON

Плюсы:

  • удобство чтения особенно в формате HOCON;

  • удобство создания делаем DSL через JSON Schema, проверяем на ошибки по мере написания;

  • переиспользование шарим между iOS и Android.

Минусы:

  • скорость придётся перед запуском считать из файла или получать с сервера;

  • время на освоение по сравнению с первым вариантом JSON Schema наверняка менее популярна.

2.3.5 Так что же лучше?

Когда делали проект, даже не изучали альтернативы. Сразу сделали через Gradle. На мой взгляд, JSON + Schema его побеждает. Удобство чтения приоритет, при этом удобство создания остаётся на том же уровне, если не лучше. Дополнительная секунда для загрузки файла на общем фоне незначительна.

Сделали конфиг через Gradle, не изучая альтернатив. Но оказалось, что JSON Schema удобнее для чтения и это её главное преимущество.

2.3.6 Best practices для buildConfigField

Если выбрали buildConfigField, то в сыром виде с ним будут проблемы:

  1. чтобы использоватьEnum, придётся указать полный путь к пакету как в типе, так и в значениях;

  2. при изменение имени или типа переменной придётся делать Find & Replace по всем конфигам.

Решение: DSL на минималках. Заводим переменные для названий параметров, а также кастомных типов и вариантов значений. Создаём отдельный gradle-скрипт на каждый модуль. Параметры описываем в формате экран-параметр-переменные. Скрипты кладём в папку business_rules.

Пример: модуль лояльности loyalty_business_rules.gradle:

/*_______________ENTER USER ID________________*//*________User ID________*//*__Variable__*/ext.USER_ID_VAR = "USER_ID"ext.USER_ID_TYPE = "com.example.whitelabelexample.domain.models.UserIdType"/*__Values__*/ext.UI_PHONE = USER_ID_TYPE + ".PHONE"ext.UI_EMAIL = USER_ID_TYPE + ".EMAIL"/*_______________NO CARD________________*//*________Obtain card methods________*//*__Variable__*/ext.OBTAIN_METHODS_VAR = "OBTAIN_CARD_METHODS"ext.OBTAIN_METHODS_ENUM = "com.example.whitelabelexample.domain.models.ObtainCardMethod"ext.OBTAIN_METHODS_TYPE = "java.util.List<" + OBTAIN_METHODS_ENUM + ">"/*__Optional values__*/ext.OM_GENERATE = OBTAIN_METHODS_ENUM + ".GENERATE_VIRTUAL"ext.OM_BIND = OBTAIN_METHODS_ENUM + " .BIND_PHYSICAL"...

UI_PHONE что за UI_? Это сокращение переменной UserId: добавляем префиксы, чтобы избежать коллизий.

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

Пример: flavor_loyaka.gradle:

...loyaka {    ...    /* MAIN SCREEN */    buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_CARD    /* MODULES */    buildConfigField APP_MODULES_TYPE, APP_MODULES_VAR, list(AM_LOYALTY, AM_SHOWCASE)    /* REGISTRATION */    buildConfigField USER_ID_TYPE, USER_ID_VAR, UI_EMAIL    ...}

flavor_jewelry.gradle:

...jewelry {    ...    /* MAIN SCREEN */    buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_SHOPS    /* MODULES */    buildConfigField APP_MODULES_TYPE, APP_MODULES_VAR, list(AM_LOYALTY, AM_SHOPS)    /* REGISTRATION */    buildConfigField USER_ID_TYPE, USER_ID_VAR, UI_PHONE    ...}

2.3.7 Получаем доступ к конфигу

Спроектируем решение в контексте Clean Architecture.

Классы конфигов приравниваю к источникам в слое data, ибо они только предоставляют данные. Тогда ui получает параметры посредством domain.

Как сгруппировать параметры по классам? Мы на проекте за группу взяли экран, а зря. С одной стороны, компактные классы, а с другой возникли коллизии, например на экранах ввода номера телефона и подтверждения кода нужна маска номера. Пришлось реализовать считывание из конфига 2 раза.

С BuildConfig это легко, но с JSON будет грязно. Считаю, что оптимально группировать по процессу (флоу). Под процессом здесь понимаю целевой use case и вторичные по отношению к нему. Обычно это группа экранов, например в модуле лояльности два целевых процесса:

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

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

Пример реализации конфига для второго процесса: фрагмент BuildCardConfig.kt:

class BuildCardConfig : CardConfig {    override fun numberMask(): String = BuildConfig.CARD_NUMBER_MASK    override fun barcodeType(): BarcodeType = BuildConfig.BARCODE_TYPE    override fun obtainmentMethods(): List<ObtainCardMethod> = BuildConfig.OBTAIN_CARD_METHODS    ...}

В итоге получим архитектуру работы с конфигом (диаграмма классов UML; в ui MVVM):

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

2.3.8 Валидируем конфиг

Зачем нужна прослойка в domain? Она же будет пустая! Необязательно. В идеале, хотим защиту от дурака проверку параметров фичей на непротиворечивость. Допустим, дано 2 параметра:

  1. включённые модули;

  2. главный экран.

Если модуль новости и акции выключён, то логично, что главным экраном новости быть не может. Но на уровне Gradle или JSON Schema подобное ограничение сделать нетривиально таким правилам и место в domain.

Например, реализуем описанное условие в GetMainTabUseCase.kt:

class GetMainTabUseCase(    private val mainConfig: MainConfig) {    operator fun invoke(): NavigationTab {        val mainTab = mainConfig.mainTab()        val mainModule = tabsByModules.entries.find { it.value == mainTab }!!.key        val isModuleEnabled = BuildConfig.APP_MODULES.contains(mainModule)        if (isModuleEnabled.not()) {            throw IllegalStateException("Can't use a tab ($mainTab) as main, it's module is disabled   fix config!")        }        return mainTab    }}

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

Альтернатива создавать UseCase только по надобности, но тогда возникает неоднородность: в ui используются одновременно и Config и UseCase. Рискуем использовать параметры, которые требуют валидации, в её обход, и следом за этим растёт вероятность багов.

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

2.4 Настраиваем фичи

2.4.1 Выбираем модули

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

Модуль здесь крупный связный и независимый кусок функциональности.

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

Рассмотрим главные точки связи модуля с приложением:

  1. Переходы изui боттом навигация, рандомная кнопка, etc;

  2. Реакция на события кастомные (выбран город), платформы (найдена сеть), etc.

По опыту, обычно хватит убрать пункт модуля из меню, например, для лояльности это таб из боттом навигации. Я реализовал пример в MainViewModel и MainActivity тестового проекта.

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

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

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

2.4.2 Настраиваем экраны и бизнес-правила

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

На уровне UseCase берём параметр из нужного класса Config.

GetCardUseCase.kt:

class GetCardUseCase(    private val netRep: CardNetRepository,    private val storageRep: CardStorageRepository,    private val config: CardConfig) {    operator fun invoke(): Card? {        return if (config.isCacheCard()) {            try {                val card = netRep.getCard()                storageRep.save(card)                card            } catch (exception: Exception) {                return storageRep.get()            }        } else {            netRep.getCard()        }    }}

В ui же обращаемся к UseCase на уровне ViewModel или Presenter.

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

Реализация: NoCardViewModel.kt:

class NoCardViewModel(    private val getObtainMethodsUseCase: GetObtainMethodsUseCase,    ...){    private val cardObtainMethods by lazy { getObtainMethodsUseCase() }    val isShowGetVirtualButton by lazy {        cardObtainMethods.contains(ObtainCardMethod.GENERATE_VIRTUAL)    }    val isShowBindPlasticButton by lazy {        cardObtainMethods.contains(ObtainCardMethod.BIND_PHYSICAL)    }    ...}

fragment_nocard.xml:

...<com.google.android.material.button.MaterialButton    android:id="@+id/no_card_bind_plastic_button"    ...    app:isVisible="@{viewmodel.isShowBindPlasticButton}" /><com.google.android.material.button.MaterialButton    android:id="@+id/no_card_get_virtual_button"    ...    app:isVisible="@{viewmodel.isShowGetVirtualButton}" />...

2.4.3 Ещё один трюк

Иногда вариативную вёрстку целесообразнее сделать без конфига.

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

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

3 Подведём итог

Мы успешно спроектировали архитектуру White Label android-проекта, которая соответствует поставленным требованиям, а именно позволяет:

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

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

Горькими уроками поделились, best practices передали. Надеюсь, наш опыт создал цельное представление о создании White Label android-приложений и комфортную отправную точку для вашего проекта.

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

Если возникли вопросы пишите в комменты, буду рад ответить!

4 Куда развить решение?

  • Мы продаём целую систему, а что если продавать модули в другие приложения? Тот же White Label, но на системный уровень ниже. Мы такую задачу решали, если интересно напишите в комментах, расскажем.

  • Когда количество приложений растёт, хочется CI и CD. В этом репозитории есть подробный гайд по настройке Azure Devops.

  • Если не нужна детальная настройка фичей, а писать flavors руками надоело сделайте автогенерацию flavors по json конфигу.

  • Бизнес бьёт ключом, клиентов больше сотни? Пора автоматизировать создание приложений.

Если знаете кейсы, на которые нет ссылок в нашей статье, то обязательно скиньте их в комменты вместе мы точно соберём крутую библиографию!

P.S. Shout-out дорогим коллегам за работу над проектом и помощь в написании статьи - без вас это было бы невозможно :)

Подробнее..

Как заблокировать приложение с помощью runBlocking

10.02.2021 14:12:50 | Автор: admin

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

Напишите где-нибудь в UI потоке (например в методе onStart) такой код:

//где-то в UI потокеrunBlocking(Dispatchers.Main) {  println(Hello, World!)}

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


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

//где-то в UI потокеHandler().post {println("Hello, World!") // отработает в UI потоке}

Или даже так:

//где-то в UI потокеrunOnUiThread {  println("Hello, World!") // и это тоже отработает в UI потоке}

Вроде конструкция очень похожа на наш проблемный код, но здесь обе части кода работают (по-разному под капотом, но работают). Чем они отличаются от кода с runBlocking?

Как работает runBlocking

Для начала небольшой дисклеймер. runBlocking редко используется в продакшн коде Android-приложения. Обычно он предназначен для использования в синхронном коде, вроде функций main или unit-тестах.

Несмотря на это, мы всё-таки рассмотрим этот билдер при вызове в главном потоке Android-приложения потому, что:

  • Это наглядно. Ниже мы придем к тому, что это актуально и не только для UI-потока Android-приложения. Но для наглядности лучше всего подходит пример на UI-потоке.

  • Интересно разобраться, почему всё именно так работает.

  • Всё-таки иногда мы можем использовать runBlocking, пусть даже в тестовых приложениях.

Билдер runBlocking работает почти так же, как и launch: создает корутину и вызывает в ней блок кода. Но чтобы сделать вызов блокирующим runBlocking создает особую корутину под названием BlockingCoroutine, у которой есть дополнительная функция joinBlocking(). runBlocking вызывает joinBlocking() сразу же после запуска корутины.

Фрагмент из runBlocking():

// runBlocking() function// val coroutine = BlockingCoroutine<T>(newContext, )coroutine.start(CoroutineStart.DEFAULT, coroutine, block)return coroutine.joinBlocking()

Функция joinBlocking() использует механизм блокировки Java LockSupport для блокировки текущего потока с помощью функции park(). LockSupport это низкоуровневый и высокопроизводительный инструмент, обычно используется для написания собственных блокировок.

Кроме того, BlockingCoroutine переопределяет функцию afterCompletion(), которая вызывается после завершения работы корутины.

override fun afterCompletion(state: Any?) {//wake up blocked threadif (Thread.currentThread ()! = blockedThread)LockSupport.unpark (blockedThread)}

Эта функция просто разблокирует поток, если она была заблокирована до этого с помощью park().

Как это всё работает примерно показано на схеме работы runBlocking.

Что здесь делает Dispatchers

Хорошо, мы поняли, что делает билдер runBlocking. Но почему в одном случае он блокирует UI-поток, а в другом нет? Почему Dispatchers.Main приводит к дедлоку...

// Этот код создает дедлокrunBlocking(Dispatchers.Main) {  println(Hello, World!)}

...,а Dispatchers.Default нет?

// А этот код создает дедлокrunBlocking(Dispatchers.Default) {  println(Hello, World!)}

Для этого вспомним, что такое диспатчер и зачем он нужен.

Диспатчер определяет, какой поток или потоки использует корутина для своего выполнения. Это некий высокоуровневый аналог Java Executor. Мы даже можем создать диспатчер из Executorа с помощью удобного экстеншна:

public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher

Dispatchers.Default реализует класс DefaultScheduler и делегирует обработку исполняемого блока кода объекту coroutineScheduler. Его функция dispatch() выглядит так:

override fun dispatch (context: CoroutineContext, block: Runnable) =  try {    coroutineScheduler.dispatch (block)  } catch (e: RejectedExecutionException) {    //    DefaultExecutor.dispatch(context, block)  }

Класс CoroutineScheduler отвечает за наиболее эффективное распределение обработанных корутин по потокам. Он реализует интерфейс Executor.

override fun execute(command: Runnable) = dispatch(command)

А что же делает функция CoroutineScheduler.dispatch()?

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

  • Создает воркеры. Воркер это класс, унаследованный от обычного Java Thread (в данном случае daemon thread). Здесь создаются рабочие потоки. У воркера также есть локальная и глобальная очереди, из которых он выбирает задачи и выполняет их.

  • Запускает воркеры.

Теперь соединим всё, что разобрали выше про Dispatchers.Default, и напишем, что происходит в целом.

  • runBlocking запускает корутину, которая вызывает CoroutineScheduler.dispatch().

  • dispatch() запускает воркеры (под капотом Java потоки).

  • BlockingCoroutine блокирует текущий поток с помощью функции LockSupport.park().

  • Исполняемый блок кода выполняется.

  • Вызывается функция afterCompletion(), которая разблокирует текущий поток с помощью LockSupport.unpark().

Эта последовательность действий выглядит примерно так.

Перейдём к Dispatchers.Main

Это диспатчер, который создан специально для Android. Например, при использовании Dispatchers.Main фреймворк бросит исключение, если вы не добавляете зависимость:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:..*'

Перед началом разбора Dispatchers.Main стоит поговорить о HandlerContext. Это специальный класс, который добавлен в пакет coroutines для Android. Это диспатчер, который выполняет задачи с помощью Android Handler всё просто.

Dispatchers.Main создаёт HandlerContext с помощью AndroidDispatcherFactory через функцию createDispatcher().

override fun createDispatcher() =  HandlerContext(Looper.getMainLooper().asHandler(async = true))

И что мы тут видим? Looper.getMainLooper().asHandler() означает, что он принимает Handler главного потока Android. Получается, что Dispatchers.Main это просто HandlerContext с Handlerом главного потока Android.

Теперь посмотрим на функцию dispatch() у HandlerContext:

override fun dispatch(context: CoroutineContext, block: Runnable) {  handler.post(block)}

Он просто постит исполняемый код через Handler. В нашем случае Handler главного потока.

Итого, что же происходит?

  • runBlocking запускает корутину, которая вызывает CoroutineScheduler.dispatch().

  • dispatch() отправляет исполняемый блок кода через Handler главного потока.

  • BlockingCoroutine блокирует текущий поток с помощью функции LockSupport.park().

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

  • Из-за этого afterCompletion() никогда не вызывается.

  • И из-за этого текущий поток не будет разблокирован (через unparked) в функции afterCompletion().

Эта последовательность действий выглядит примерно так.

Вот почему runBlocking с Dispatchers.Main блокирует UI-поток навсегда.

Главный потокблокируется и ждёт завершения исполняемого кода. Но он никогда не завершается, потому что Main Looper не может получить сообщение на запуск исполняемого кода. Дедлок.

Совсем простое объяснение

Помните пример с Handler().post в самом начале статьи? Там код работает и ничего не блокируется. Однако мы можем легко изменить его, чтобы он был в значительной степени похож на наш код с Dispatcher.Main, и стал ещё нагляднее. Для этого можем добавить операции parking и unparking к текущему потоку, иммитируя работу функций afterCompletion() и joinBlocking(). Код начинает работать почти так же, как с билдером runBlocking.

//где-то в UI потокеval thread = Thread.currentThread()Handler().post {  println("Hello, World!") // это никогда не будет вызвано  // имитируем afterCompletion()  LockSupport.unpark(thread)}// имитируем joinBlocking()LockSupport.park()

Но этот трюк не будет работать с функцией runOnUiThread.

//где-то в UI потокеval thread = Thread.currentThread()runOnUiThread {  println("Hello, World!") // этот код вызовется  LockSupport.unpark(thread)}LockSupport.park()

Это происходит потому, что runOnUiThread использует оптимизацию, проверяя текущий поток. Если текущий поток главный, то он сразу же выполнит блок кода. В противном случае сделает post в Handler главного потока.

Если всё же очень хочется использовать runBlocking в UI-потоке, то у Dispatchers.Main есть оптимизация Dispatchers.Main.immediate. Там аналогичная логика как у runOnUiThread. Поэтому этот блок кода будет работать и в UI-потоке:

//где-то в UI потокеrunBlocking(Dispatchers.Main.immediate) {   println(Hello, World!)}

Выводы

В статье я описал как безобидный билдер runBlocking может заморозить ваше приложение на Android. Это произойдет, если вызвать runBlocking в UI-потоке с диспатчером Dispatchers.Main. Приложение заблокируется по следующему алгоритму:

  • runBlocking создаёт блокирующую корутину BlockingCoroutine.

  • Dispatchers.Main отправляет на запуск исполняемый блок кода через Handler.post.

  • Но BlockingCoroutine тут же заблокирует UI поток.

  • Поэтому Main Looper никогда не получит сообщение с исполняемым блоком кода.

  • А UI не разблокируется, потому что корутина ждёт завершения исполняемого кода.

Эта статья больше теоретическая, чем практическая. Просто потому, что runBlocking редко встречается в продакшн-коде. Но примеры с UI-потоком наглядны, потому что можно сразу заблокировать приложение и разобраться, как работает runBlocking.

Но заблокировать исполнение можно не только в UI-потоке, но и с помощью других диспатчеров, если поток вызова и корутины окажется одним и тем же. В такую ситуацию можно попасть, если мы будем пытаться вызвать билдер runBlocking на том же самом потоке, что и корутина внутри него. Например, мы можем использовать newSingleThreadContext для создания нового диспатчера и результат будет тот же. Здесь UI не будет заморожен, но выполнение будет заблокировано.

val singleThreadDispatcher = newSingleThreadContext("Single Thread")GlobalScope.launch (singleThreadDispatcher) {  runBlocking (singleThreadDispatcher) {    println("Hello, World!") // этот кусок кода опять не выполнится  }}

Если очень надо написать runBlocking в главном потоке Android-приложения, то не используйте Dispatchers.Main. Используйте Dispatchers.Default или Dispatchers.Main.immediate в крайнем случае.


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

Оригинал статьи на английском How runBlocking May Surprise You.
Как страдали iOS-ники когда выпиливали Realm.
О том, над чем в целом мы тут работаем: монолит, монолит, опять монолит.
Кратко об истории Open Source просто развлечься (да и статья хорошая).

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

Подробнее..

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

14.02.2021 14:06:25 | Автор: admin
В новом дайджесте локализация и кастомные плагины, защита прав и неготовность Flutter, документация и тестирование, доходы подписок и легендарный симулятор Кобаяси Мару. Подключайтесь!



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

iOS

Создание пользовательских функций запросов с key paths
Процесс локализации iOS приложения в компании Vivid Money
AppsFlyer запускает предиктивную аналитику для iOS
Начинаем работу с Combine
Мошенничество в App Store: разработчик раскрывает многомиллионные аферы с приложениями
Hyundai не ведет переговоров с Apple
Советы по реализации темного режима в iOS
Набор инструментов iOS-разработчика на 2021 год
Поиск лучшего CI/CD для разработки под iOS
Swift простая обработка ошибок
Уловки iOS: автоматическая обработка клавиатуры
5 способов улучшить рабочий процесс в Xcode
Быстрое погружение в iOS-разработку
SPPermissions: получение разрешений в Swift
PermissionsSwiftUI: получение разрешений в SwiftUI

Android

SafetyNet Attestation описание и реализация проверки на PHP
Как заблокировать приложение с помощью runBlocking
Android Broadcast: новости #3
Чем различаются Dagger, Hilt и Koin
Android туториал: учим CRUD
Готовимся к декларативному UI
Три вещи, которые я перестала делать вручную как Android-разработчик
Внедрение нативного кода Kotlin во Flutter-приложение
ShapeableView в Jetpack Compose
Как мы ускорили запуск приложения Dropbox для Android на 30%
Компоненты Android View Binding: Диалоги и Адаптеры
GitHub Actions для Android-разработчиков
Expenso: контроль расходов
Auxio: плеер для Android

Разработка

Как создать кастомный плагин для Dart-анализатора
Защита авторских прав на ваши Pet-projects
На GitHub предлагают запустить каталог мобильных приложений
Выпускные проекты: как позаботиться о себе, завести питомца, найти пункт переработки и получить ответ на любой вопрос
Match-3 Framework это просто
Магия асинхронных операций: взгляд изнутри. Future
Connected! Самое главное о дизайне VPN-приложения
Mockito. Из чего он приготовлен и как его подавать?
Микромодульный подход к дизайну продукта
Стики и работа с Event System в Unity 3D
Какая бывает документация
Мобильное тестирование, автоматизация тестирования, тестирование API: с чем нужно уметь работать в 2021 году
Что разработчику нужно знать о работе с дизайном/дизайнером
Принципы нарративного дизайна
Оценка трудозатрат в веб- и мобильных проектах
Flutter. Асинхронность (async) <> параллельность (isolate). Совсем
Работа с адаптивным программируемым интерфейсом APIs во Flutter
Podlodka #202: офисная политика
Flutter Dev Podcast #24: Dart Null Safety
Flutter пока не смог стать надежным кроссплатформенным решением
Как попасть в геймдев: 5 игр, с которых стоит начать свой путь в разработке игр
Cocos переходит в 3D
Дизайн приложений: примеры для вдохновения #31
Эстетический и минималистичный дизайн как часть юзабилити
Unity за 1 минуту
Учимся программировать и писать игры на Nintendo Game Boy
Как 3 месяца парного программирования повлияли на мою карьеру разработчика
Устали от императивных циклов For? Используйте функциональные операторы
Объектно-ориентированное мышление слишком сложно для вас
Создание IoT-приложения, совместимого со смарт-устройствами, на Flutter
Ускоряем разработки приложений с помощью Flutter
20 лучших движков и платформ/инструментов для разработки мобильных игр в 2021 году
Как Material Design помогает брендировать ваше приложение
Как вести переговоры продуктового дизайнера и разработчика
Boardgame.io: движок для пошаговых игр

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

Scopely запустила симулятор Кобаяси Мару
Отчет Flurry 2021 State of Mobile
Расходы в Топ-100 приложений с подпиской выросли на 34% до $13 млрд
App Annie Pulse: инсайты рынка приложений
78% пользователей отказывалось от покупки, если требовалась установка приложения
Новый рейтинг мобильных рекламных сетей Singular 2021 ROI Index
Роскомнадзор выпустил мобильное приложение
Apple начала показывать рекламу на странице поиска
Electronic Arts покупает Glu Mobile
Beam: осмысленный браузер
Blizzard готовит несколько мобильных игр World of Warcraft
Как увеличить revenue мобильного приложения на 10-15% с помощью специальных инструментов от Apple
Оптимизируйте удержание приложений с помощью модели Hooked
20 ужасных ASO-ошибок, которых нужно избежать в 2021

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

ESP32-C3: первое знакомство. Заменим ESP8266?
Как машинное обучение и TensorFlow помогают готовить гибридную выпечку: хобби-кейс разработчика Google
Как построить AI-друга. Расшифровка доклада

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

Перевод Разработка на Android как найти подходящую абстракцию для работы со строками

15.02.2021 12:06:03 | Автор: admin

В своих проектах мы стараемся по мере необходимости покрывать код тестами и придерживаться принципов SOLID и чистой архитектуры. Хотим поделиться с читателями Хабра переводом статьи Hannes Dorfmann автора серии публикаций об Android-разработке. В этой статье описан способ, который помогает абстрагировать работу со строками, чтобы скрыть детали взаимодействия с разными типами строковых ресурсов и облегчить написание юнит-тестов.

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

Фото: UnsplashФото: Unsplash

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

Уровень абстракции для строк?

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

  • Простой строковый ресурс вроде R.string.some_text, отображаемый на экране с помощью resources.getString(R.string.some_text)

  • Отформатированная строка, которая форматируется во время выполнения, т.е. context.getString(R.string.some_text, arg1, 123) с

<string name=some_formatted_text>Some formatted Text with args %s %i</string>
  • Более сложные строковые ресурсы, такие как Plurals, которые перегружены, например resources.getQuantityString(R.plurals.number_of_items, 2):

<plurals name="number_of_items">  <item quantity="one">%d item</item>  <item quantity="other">%d items</item></plurals>
  • Простой текст, который не загружается из ресурсов Android в XML-файле вроде strings.xml, а уже загружен в переменную типа String и не требует дальнейшего преобразования (в отличие от R.string.some_text). Например, фрагмент текста, извлеченный из json ответа с сервера.

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

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

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

Давайте шаг за шагом рассмотрим эти моменты на конкретном примере: предположим, мы хотим загружать строку с сервера по http, и если это не удается, мы отображаем аварийную fallback-строку из strings.xml. Например, так:

class MyViewModel(  private val backend : Backend,  private val resources : Resources // ресурсы Android из context.getResources()) : ViewModel() {  val textToDisplay : MutableLiveData<String>  // MutableLiveData используется для удобства чтения   fun loadText(){    try {      val text : String = backend.getText()       textToDisplay.value = text    } catch (t : Throwable) {      textToDisplay.value = resources.getString(R.string.fallback_text)    }  }}

Детали реализации просочились в нашу MyViewModel, что в целом усложняет ее тестирование. Действительно, чтобы написать тест для loadText(), нам надо либо замокать Resources, либо ввести интерфейс наподобие StringRepository (по шаблону "репозиторий"), чтобы при тестировании мы могли заменить его другой реализацией:

interface StringRepository{  fun getString(@StringRes id : Int) : String} class AndroidStringRepository(  private val resources : Resources // ресурсы Android из context.getResources()) : StringRepository {  override fun getString(@StringRes id : Int) : String = resources.getString(id)} class TestDoubleStringRepository{    override fun getString(@StringRes id : Int) : String = "some string"}

Затем вью-модель получит StringRepository вместо непосредственно ресурсов, и в этом случае все будет в порядке, не так ли?

class MyViewModel(  private val backend : Backend,  private val stringRepo : StringRepository // детали реализации скрываются за интерфейсом) : ViewModel() {  val textToDisplay : MutableLiveData<String>     fun loadText(){    try {      val text : String = backend.getText()       textToDisplay.value = text    } catch (t : Throwable) {      textToDisplay.value = stringRepo.getString(R.string.fallback_text)    }  }}

На эту вью-модель можно написать такой юнит-тест:

@Testfun when_backend_fails_fallback_string_is_displayed(){  val stringRepo = TestDoubleStringRepository()  val backend = TestDoubleBackend()  backend.failWhenLoadingText = true // заставит backend.getText() выкинуть исключение  val viewModel = MyViewModel(backend, stringRepo)  viewModel.loadText()   Assert.equals("some string", viewModel.textToDisplay.value)}

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

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

  • Кроме того, если рассматривать реализацию TestDoubleStringRepository и тест, который мы написали, насколько он является значимым? TestDoubleStringRepository всегда возвращает одну и ту же строку. Мы могли бы совершенно испортить код вью-модели, передавая R.string.foo вместо R.string.fallback_text в StringRepository.getString(), и наш тест все равно бы был пройден. Конечно, можно улучшить TestDoubleStringRepository, чтобы он не просто всегда возвращал одну и ту же строку:

class TestDoubleStringRepository{    override fun getString(@StringRes id : Int) : String = when(id){      R.string.fallback_test -> "some string"      R.string.foo -> "foo"      else -> UnsupportedStringResourceException()    }}

Но насколько это поддерживаемо? Вы хотели бы так делать для всех строк в вашем приложении (если их у вас сотни)?

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

Нам поможет TextResource

Придуманная нами абстракция называется TextResource. Это модель для представления текста, которая относится к слою domain. Таким образом, это объект первого класса в нашей бизнес-логике. И выглядит это следующим образом:

sealed class TextResource {  companion object { // Используется для статических фабричных методов, чтобы файл с конкретной реализацией оставался приватным    fun fromText(text : String) : TextResource = SimpleTextResource(text)    fun fromStringId(@StringRes id : Int) : TextResource = IdTextResource(id)    fun fromPlural(@PluralRes id: Int, pluralValue : Int) : TextResource = PluralTextResource(id, pluralValue)  }} private data class SimpleTextResource( // Можно будет также использовать inline классы  val text : String) : TextResource() private data class IdTextResource(  @StringRes id : Int) : TextResource() private data class PluralTextResource(    @PluralsRes val pluralId: Int,    val quantity: Int) : TextResource() // можно будет добавить и другие виды текста...

Так выглядит вью-модель с TextResource:

class MyViewModel(  private val backend : Backend // Обратите, пожалуйста, внимание, что не надо передавать ни какие-то ресурсы, ни StringRepository.) : ViewModel() {  val textToDisplay : MutableLiveData<TextResource> // Тип уже не String     fun loadText(){    try {      val text : String = backend.getText()       textToDisplay.value = TextResource.fromText(text)    } catch (t : Throwable) {      textToDisplay.value = TextResource.fromStringId(R.string.fallback_text)    }  }}

Основные отличия:

1) textToDisplay поменялся c LiveData<String> на LiveData<TextResource>, поэтому теперь вью-модели не нужно знать, как переводить разные типы текста в String. Она должна уметь переводить их в TextResource. Однако, это нормально, как будет видно далее, TextResource это абстракция, которая решит наши проблемы.

2) Посмотрите на конструктор вью-модели. Нам удалось удалить неправильную абстракцию StringRepository (при этом нам не нужны Resources). Вас, возможно, интересует, как теперь писать тесты? Так же просто, как напрямую протестировать TextResource. Дело в том, что эта абстракция также абстрагирует зависимости Android, такие как ресурсы или контекст (R.string.fallback_text это просто Int). И вот как выглядит наш юнит-тест:

@Testfun when_backend_fails_fallback_string_is_displayed(){  val backend = TestDoubleBackend()  backend.failWhenLoadingText = true // заставит backend.getText() выкинуть исключение  val viewModel = MyViewModel(backend)  viewModel.loadText()   val expectedText = TextResource.fromStringId(R.string.fallback_text)  Assert.equals(expectedText, viewModel.textToDisplay.value)  // для data class-ов генерируются методы equals, поэтому мы легко можем их сравнивать}

Пока все хорошо, но не хватает одной детали: как нам преобразовать TextResource в String, чтобы можно было отобразить его, например, в TextView? Что ж, это касается исключительно отрисовки в Android, и мы можем создать функцию расширения и заключить ее в слое UI.

// Можно получить ресурсы с помощью context.getResources()fun TextResource.asString(resources : Resources) : String = when (this) {   is SimpleTextResource -> this.text // smart cast  is IdTextResource -> resources.getString(this.id) // smart cast  is PluralTextResource -> resources.getQuantityString(this.pluralId, this.quantity) // smart cast}

А поскольку преобразование TextResource в String происходит в UI (на уровне представления) архитектуры нашего приложения, TextResource будет переводиться при изменении конфигурации (т.е. при изменении системного языка на смартфоне), что обеспечит правильную локализацию строки для любых ресурсов R.string.* вашего приложения.

Бонус: вы можете легко написать юнит-тест для TextResource.asString(), создавая моки для ресурсов. При этом не следует создавать мок для каждого отдельного строкового ресурса в приложении, потому что на самом деле нужно протестировать всего лишь работу конструкции when. Поэтому здесь будет корректно всегда возвращать одну и ту же строку из замоканного resources.getString(). Кроме того, TextResource можно многократно использовать в коде, и он соответствует принципу открытости/закрытости. Так, его можно расширить для будущих вариантов использования, добавив всего несколько строк кода: новый класс данных, который расширяет TextResource, и новую ветку в конструкцию when в TextResource.asString().

Поправка: как правильно подметили в комментариях, TextResource не следует принципу открытости/закрытости. Можно было бы поддержать принцип открытости/закрытости для TextResource, если бы у sealed class TextResouce была abstract fun asString(r: Resources), которую реализуют все подклассы. Я лично считаю, что можно пожертвовать принципом открытости/закрытости в пользу упрощения структур данных и работать с расширенной функцией asString(r: Resources), которая находится за пределами иерархии наследования (именно этот способ описан в статье и является достаточно расширяемым, хотя и не настолько, как с принципом открытости/закрытости). Почему? Я считаю, что добавление функции с параметром Resources к публичному API TextResource проблематично, потому что только часть подклассов нуждается в этом параметре (например, SimpleTextResource такого вообще не требует). Кроме того, если такая реализация станет частью общедоступного API, это может привести к увеличению накладных расходов на поддержку кода, а также к появлению дополнительных сложностей (особенно при тестировании).

Выводы

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

Подробнее..

Дайджест интересных материалов для мобильного разработчика 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. Быть маневренным - золотой навык. Ваш бизнес не забуксует на месте и поток запланнированных задач реже станет оставать от графика.

Подробнее..

Перевод Flutter ListView и ScrollPhysics Детальный взгляд

04.02.2021 00:11:44 | Автор: admin

Подробное изучение виджета ListView и его особенностей.

Некоторое время назад я написал статью об основах использования ListView и GridView во Flutter. Эта статья предназначена для более детального изучения класса ListView, ScrollPhysics, а также параметров конфигурирования и оптимизаций для общего виджета.

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

Изучение типов ListView

Мы начнем с рассмотрения типов ListViews, а позже рассмотрим другие возможности и усовершенствования для него.

Рассмотрим типы ListViews:

  1. ListView

  2. ListView.builder

  3. ListView.separated

  4. ListView.custom

Давайте исследовать эти типы один за другим:

ListView

Это дефолтный конструктор класса ListView. ListView просто берет список дочерних элементов и делает из него список с возможностью прокрутки.

Список, построенный с использованием конструктора по умолчаниюСписок, построенный с использованием конструктора по умолчанию

Общий формат кода:

ListView(  children: <Widget>[    ItemOne(),    ItemTwo(),    ItemThree(),  ],),

ListView.builder()

Конструктор builder() строит повторяющийся список элементов. Конструктор принимает два основных параметра: itemCount для подсчета количества элементов в списке и itemBuilder конструктор для каждого построенного элемента списка.

Список, построенный с помощью конструктора builder()Список, построенный с помощью конструктора builder()

Общий формат кода:

ListView.builder(  itemCount: itemCount,  itemBuilder: (context, position) {    return listItem();  },),

Хитрый трюк: так как они загружаются не сразу и только необходимое количество, нам на самом деле не нужно, чтобы item Count (счетчик элементов) был в качестве обязательного параметра, таким образом список может быть бесконечным.

ListView.builder(  itemBuilder: (context, position) {    return Card(      child: Padding(        padding: const EdgeInsets.all(16.0),        child: Text(position.toString(), style: TextStyle(fontSize: 22.0),),      ),    );  },),
ListView без параметра ItemCountListView без параметра ItemCount

ListView.separated()

В конструкторе separated() мы генерируем список и можем указать разделитель между каждым элементом.

separated()separated()

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

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

Код для этого типа идет как:

ListView.separated(      itemBuilder: (context, position) {        return ListItem();      },      separatorBuilder: (context, position) {        return SeparatorItem();      },      itemCount: itemCount,),

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

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

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

Примечание: Длина списка разделителей на 1 единицу меньше, чем список элементов, так как после последнего элемента разделитель не ставится.

ListView.custom()

Конструктор custom(), как подсказывает его название, позволяет создавать ListViews с с индивидуальными функциональными возможностями для построения дочерних элементов списка. Основным параметром, необходимым для этого, является SliverChildDelegate, который формирует компоненты. Существуют следующие типы SliverChildDelegates :

  1. SliverChildListDelegate

  2. SliverChildBuilderDelegate .

SliverChildListDelegate принимает прямой дочерний список, в то время как SliverChildBuiderDelegate принимает IndexedWidgetBuilder (Функция сборщика, которую мы используем).

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

ListView.builder по сути является ListView.custom с функцией SliverChildBuilderDelegate.


Конструктор по умолчанию ListView ведет себя как ListView.custom с SliverChildListDelegate.

Теперь, когда мы закончили с типами ListViews, давайте взглянем на ScrollPhysics.

Изучение ScrollPhysics

Для управления прокруткой мы устанавливаем физический параметр в конструкторе ListView. Различные типы этого параметра:

NeverScrollableScrollPhysics .

NeverScrollScrollPhysics запрещает прокрутку списка. Используйте это, чтобы полностью отключить прокрутку ListView.

BouncingScrollPhysics .

BouncingScrollPhysics возвращает список обратно, когда список заканчивается. Аналогичный эффект используется на iOS.

ClamppingScrollPhysics

Это функция прокрутки, которая по умолчанию используется на Android. Список останавливается в конце и показывает индикацию этого действия.

FixedExtentScrollPhysics

Это немного отличается от других списков в том смысле, что работает только с FixedExtendScrollControllers и списками, которые их используют. Для примера возьмём ListWheelScrollView, который делает список, напоминающий по форме колесо.

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

Код для этого примера невероятно прост:

FixedExtentScrollController fixedExtentScrollController =    new FixedExtentScrollController();ListWheelScrollView(  controller: fixedExtentScrollController,  physics: FixedExtentScrollPhysics(),  children: monthsOfTheYear.map((month) {    return Card(        child: Row(      children: <Widget>[        Expanded(            child: Padding(          padding: const EdgeInsets.all(8.0),          child: Text(            month,            style: TextStyle(fontSize: 18.0),          ),        )),      ],    ));  }).toList(),  itemExtent: 60.0,),

Ещё несколько вещей, которые нужно знать.

Как сохранить элементы, которые удаляются при работе со списком ?

Flutter позволяет использовать виджет KeepAlive(), сохраняющий элемент, который в противном случае был бы удален. В списке элементы по умолчанию встроены в виджет AutomaticKeepAlive.

AutomaticKeepAlives можно отключить, установив в поле addAutomaticKeepAlives значение false. Это полезно в тех случаях, когда элементы не нуждаются в сохранении или в пользовательской версии KeepAlive.

Почему мой ListView имеет отступ между списком и внешним виджетом?

По умолчанию ListView имеет отступ между списком и внешним виджетом, чтобы удалить его, установите EdgeInsets.all(0.0).

Вот и все для этой статьи!

Надеюсь, вам понравилось.

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


Перевод статьи подготовлен в преддверии старта курсаFlutter Mobile Developer.


В преддверии старта курса приглашаем всех желающих записаться на бесплатны демо-урок по теме:
Графика во Flutter.

На уроке участники вместе с экспертом-ведущим разберут, как устроен рендеринг во Flutter и изучат основные компоненты библиотекиdart:ui.

Подробнее..

Envoy как универсальный сетевой примитив

05.02.2021 00:16:03 | Автор: admin

В октябре прошлого года мои коллеги представили на EnvoyCon доклад "Построение гибкой подсистемы компрессии в Envoy". Вот он ниже



Судя по статистике сегодняшней статьи от SergeAx, тема компрессии сетевого трафика оказалась интересной многим. В связи с чем я немедленно возжелал вселенской славы и решил кратко пересказать содержание доклада. Тем более, что он не только о компрессии, но и том, как можно упростить сопровождение сетевой подсистемы как backend'а, так и мобильного frontend'а.


Я не стал полностью "новелизировать" видео доклада, а только ту часть, которую озвучил Хосе Ниньо. Она заинтересует больше людей.


Для начала о том, что такое Envoy.


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



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



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



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



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


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



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



Так появился проект Envoy Mobile, который представляет собой байндинги на Java, Kotlin, Swift, Objective-C к Envoy. А тот уже линкуется к мобильному приложению как нативная библиотека.


Тогда задача уменьшения объёма трафика описанная в статье от FunCorp, могла бы быть решена примерно как на картинке ниже (если поменять местами компрессор и декомпрессор, и заменить response на request). То есть даже без необходимости установки обновлений на телефонах.



Можно пойти дальше, и ввести двустороннюю компрессию



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

Подробнее..

Фото из Android смартфона в Qt Widgets

28.02.2021 14:11:42 | Автор: admin

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

Описание проблемы

Если вы пишете кросс-платформенное приложение, то для получения изображения из камеры для ПК можно воспользоваться классом QCamera, пример для работы с которым описан в документации Qt.

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

QT += multimedia multimediawidgets

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

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

  1. использовать QML и написать свой Qt Quick-элемент, выполняющий эту функцию, затем состыковать его остальной частью приложения на Qt Widgets, С++;

  2. использовать приложение по-умолчанию Android-смартфона для получения фотографии, затем обработать её в своём приложении.

Рассмотрим первый вариант

Если вы С++ программист Qt Widgets, то очередное эпизодическое углубление в QML займёт у вас время, добавим к этому время на написание Qt Quick-элемента, стыковки этого элемента с С++ кодом, отладки написанного кода. Если вы не профессионал в QML получается долго и сложно.

Рассмотрим второй вариант

В Android-смартфоне уже есть приложение по-умолчанию, прекрасно выполняющее нужную функцию, нужно им просто воспользоваться, применив Java-вызовы (JNI Java Native Interface) из С++ кода при помощи QtAndroid. Выглядит проще. Полностью работающего кода в интернете я не нашёл, и, изучив опыт других, опираясь на документацию разработчка на Android написал собственный.

Как это сделать

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

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

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

Получение миниатюры

Начнём с .pro файла.

Он должен содержать следующие строки для поддержки Android.

android {    QT       +=androidextras}

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

Заголовочный файл (.h) класса имеет вид:

#ifndef CAMSHOT_H#define CAMSHOT_H#include <QObject>#include <QString>#include <cstring>#include <QImage>#include <QDebug>#include <QtAndroid>#include <QAndroidActivityResultReceiver>#include <QAndroidParcel>class CamShot : public QObject, public QAndroidActivityResultReceiver{    Q_OBJECTpublic:    CamShot(QObject *parent = nullptr):QObject(parent),QAndroidActivityResultReceiver(){}        static const int RESULT_OK = -1;     static const int REQUEST_IMAGE_CAPTURE = 1;    static const int REQUEST_TAKE_PHOTO = REQUEST_IMAGE_CAPTURE;    void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data)  override;    static QImage camThumbnailToQImage(const QAndroidJniObject &data);public slots:    void aMakeShot();signals:    void createNew(const QImage &img);};#endif // CAMSHOT_H

Заголовочный файл (.cpp) класса имеет вид:

QImage CamShot::camThumbnailToQImage(const QAndroidJniObject &data){    QAndroidJniObject bundle = data.callObjectMethod("getExtras","()Landroid/os/Bundle;");    qDebug()<<"bundle.isValid() "<<bundle.isValid()<<bundle.toString();    QAndroidJniObject bundleKey = QAndroidJniObject::fromString("data");    const QAndroidJniObject aBitmap (data.callObjectMethod("getParcelableExtra", "(Ljava/lang/String;)Landroid/os/Parcelable;", bundleKey.object<jstring>()));    qDebug()<<"aBitmap.isValid() "<<aBitmap.isValid()<<aBitmap.toString();    jint aBitmapWidth = aBitmap.callMethod<jint>("getWidth");    jint aBitmapHeight = aBitmap.callMethod<jint>("getHeight");    QAndroidJniEnvironment env;    const int32_t aBitmapPixelsCount = aBitmapWidth * aBitmapHeight;    jintArray pixels = env->NewIntArray(aBitmapPixelsCount);    jint aBitmapOffset = 0;    jint aBitmapStride = aBitmapWidth;    jint aBitmapX = 0;    jint aBitmapY = 0;    aBitmap.callMethod<void>("getPixels","([IIIIIII)V", pixels, aBitmapOffset, aBitmapStride, aBitmapX, aBitmapY, aBitmapWidth, aBitmapHeight);    jint *pPixels = env->GetIntArrayElements(pixels, nullptr);    QImage img(aBitmapWidth, aBitmapHeight, QImage::Format_ARGB32);    int lineSzB = aBitmapWidth * sizeof(jint);    for (int i = 0; i < aBitmapHeight; ++i){        uchar *pDst = img.scanLine(i);        const uchar *pSrc = reinterpret_cast<const uchar*>(pPixels + aBitmapWidth * i + aBitmapWidth);        memcpy(pDst, pSrc, lineSzB);    }    env->DeleteLocalRef(pixels); //env->ReleaseIntArrayElements(pixels, pPixels, 0); отвязывает указатель на данные массива от массива, а надо удалить сам массив, поэтому DeleteLocalRef.    return img;}void CamShot::aMakeShot() {    QAndroidJniObject action = QandroidJniObject::fromString("android.media.action.IMAGE_CAPTURE");    //Если необходимо указать Java-класс (не аргумент функции), то указывается полное имя класса (точки-разделители заменяются на "/"), например  "android/content/Intent", "java/lang/String".    //Если аргумент функции Java-объект, то писать имя класса начиная с "L" и ";" в конце, например "Landroid/content/Intent ;", "Ljava/lang/String;".    //Если примитивный тип или массив, то указываются соответствующие символы без разделителей, например "V" (void) или "[IIIIIII" (массив jint, и 6 jint за ним)    //Символы, соответствия примитивны типам:    QAndroidJniObject intent=QAndroidJniObject("android/content/Intent","(Ljava/lang/String;)V", action.object<jstring>());    QtAndroid::startActivity(intent, REQUEST_IMAGE_CAPTURE, this);}void CamShot::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data){    if ( receiverRequestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK )    {        const QImage thumbnail (camThumbnailToQImage(data));        if (!thumbnail.isNull())            emit createNew(thumbnail);    }}

Разберём приведённый код

Краткие правила указания аргументов в JNI-вызовах

  1. если необходимо указать имя Java-класса (не в качестве аргумента Java-функции), то указывается полное имя класса (точки-разделители заменяются на "/"), например "android/content/Intent", "java/lang/String";

  2. если аргумент функции Java-объект, то писать имя его класса начиная с "L" и ";" в конце, например "Landroid/content/Intent;", "Ljava/lang/String;";

  3. если примитивный тип или массив, то указываются соответствующие сигнатуры (символы без разделителей), например "V" (void), "I" (jint) или "[IIIIIII" (массив jint, и 6 jint за ним);

  4. сигнатуры примитивных типов:

    C/C++

    JNI

    Java

    Signature

    uint8_t/unsigned char

    jboolean

    bool

    Z

    int8_t/char/signed char

    jbyte

    byte

    B

    uint16_t/unsigned short

    jchar

    char

    C

    int16_t/short

    jshort

    short

    S

    int32_t/int/(long)

    jint

    int

    I

    int64_t/(long)/long long

    jlong

    long

    J

    float

    jfloat

    float

    F

    double

    jdouble

    double

    D

    void


    void

    V

  5. сигнатуры массивов:

    JNI

    Java

    Signature

    jbooleanArray

    bool[]

    [Z

    jbyteArray

    byte[]

    [B

    jcharArray

    char[]

    [C

    jshortArray

    short[]

    [S

    jintArray

    int[]

    [I

    jlongArray

    long[]

    [L

    jfloatArray

    float[]

    [F

    jdoubleArray

    double[]

    [D

    jarray

    type[]

    [Lfully/qualified/type/name;

    jarray

    String[]

    [Ljava/lang/String;


    Чтобы получить доступ к элементам массива, необходимо использовать JNI-методы объекта класса QAndroidJniEnvironment, например такие как: NewIntArray, GetIntArrayElements, DeleteLocalRef GetArrayLength,GetObjectArrayElement, SetObjectArrayElement, и т.д.

Подробнее можно прочитать в презентации (pdf) Practical Qt on Android JNI qtcon.

В заголовочном файле class CamShot содержит:

  1. значения констант, взятых их документации разработчка Android (так код короче и меньше Java-вызовов);

  2. переопределение абстрактного метода void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override; в который будет передаваться Java-объект класса Intent с миниатюрой изображения;

  3. статический метод
    static QImage camThumbnailToQImage(const QAndroidJniObject &data);
    извлекающий из Java-объекта класса Intent Java-объект класса Bitmap, копирующий пиксели в массив пкселей (32-битных значений) и построчно копирующий эти пиксели в QImage;

  4. общедоступный слот
    void aMakeShot();
    вызывающий операцию по фотографированию изображения и получению его миниатюры;

  5. сигнал
    void createNew(const QImage &img);
    высылающий полученную миниатюру потребителю.

В методе void aMakeShot() создаётся Java-объект Intent в который передаётся строка со значением, указывающим, что необходимо сделать произвести захват изображения. После этого сформированное действие (Intent) отправляется на исполнение (Activity).

В процессе выполнения действия будет запущено приложение по-умолчанию для фотографирования. Как только фотография будет сделана и подтверждена пользователем, будет произведён вызов виртуального метода handleActivityResult, в котором осуществляется проверка: является ли выполненное действие запрошенным и успешно выполненным. Если да, то вызовем статический метод camThumbnailToQImage получения изображения QImage из Java-объекта класса Bitmap и при успешном результате отправим полученное изображение потребителю сигналом Qt.

Рассмотрим статический метод
static QImage camThumbnailToQImage(const QAndroidJniObject &data) override;

Интересующее нас изображение передаётся в блоке дополнительных данных Java-объекта класса Intent и является Java-объектом класса Bundle, чтобы его получить нужно воспользоваться методом объекта Intent:
Bundle getExtras()

В Bundle хранятся ассоциативные пары <ключ-строка>:<значение>. В статье получить фотографии на Android указан ключ, по которому располагается миниатюра. Это строка "data".

Получим Java-объект класса Bitmap по ключу, воспользовавшись методом объекта Intent:
T getParcelableExtra (String name)

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

Для переноса значений пикселей из Bitmap в QImage воспользуемся методом объекта Bitmap:
void getPixels (int[] pixels, int offset, int stride, int x, int y, int width, int height)

Для этого понадобится создать линейный массив значений пикселей
jintArray pixels = env->NewIntArray(aBitmapPixelsCount);
После того, как пиксели будут скопированы, получим указатель на начало массива, который можно использовать в C++ коде:
jint *pPixels = env->GetIntArrayElements(pixels, nullptr);
Затем в цикле построчно скопируем значения пикселей из массива в изображение Qimage. По завершению копирования освобождаем память, выделенную под массив значений пикселей
env->DeleteLocalRef(pixels);
и возвращаем результат в виде QImage.

Отлично. Миниатюра изображения получена.

Получение полноразмерного изображения

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

  1. androidx.core.content.FileProvider;

  2. android.support.v4.content.FileProvider.

Первый самый современный, не поддерживается Qt, а для использования второго необходимо настроить среду QtCerator:

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

Главное меню (сверху) Инструменты Параметры Устройства вкладка Android вкладка SDK ManagerРазвернуть элемент списка Инструменты в список Extras Android Support Repository - поставить флажок установить и нажить на кнопку Применить справа.

Заменить автогенерируемые файлы настройки сборки для Android собственными

Перейти на боковой панели QtCreator на вкладку Проекты. В левой области окна Сборка и запуск Сборка. Тогда в правой области окна Build Android APK Create Templates. В появившемся диалоговом окне установить флажок Копировать файлы Gradle в каталог Android, нажать на кнопку Завершить:

Добавить каталог со своими настройками сборки в проект

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

Настроить в файле проекта отключаемую возможность поддержки Android

Если приложение кросс-платформенное и предполагается компиляция не только на Android, то в .pro файле необходимо добавить директиву android: перед каждым добавленным файлом:

android {    QT       +=androidextras}#  DISTFILES += \android:    android/AndroidManifest.xml \android:    android/build.gradle \android:    android/gradle/wrapper/gradle-wrapper.jar \android:    android/gradle/wrapper/gradle-wrapper.properties \android:    android/gradlew \android:    android/gradlew.bat \android:    android/res/values/libs.xml \    todo.txt

Отредактировать AndroidManifest.xml

Отредактировать файл AndroidManifest.xml в android/AndroidManifest.xml, добавив в секцию после

</activity><!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices ->

текст:

<provider android:name="android.support.v4.content.FileProvider" android:authorities="org.qtproject.example.qsketch.fileprovider" android:grantUriPermissions="true" android:exported="false"><meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/></provider>

Создать файл с указанием каталога совместного использования с другими приложениями

Это нужно для того, чтобы приложение фотографирования по-умолчанию могло передать нашему приложению файл.

В каталоге сборки, там где находится автогенерируемый файл AndroidManifest.xml внутри каталога res рядом с каталогом values, создать каталог xml, а в нём файл file_paths.xml ( /abin/AndroidManifest.xml) ( /abin/res/xml/file_paths.xml). В созданный файл поместить следующие строки:

<?xml version="1.0" encoding="utf-8"?><paths xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"><external-files-path name="shared" path="shared/" /></paths>

где shared/ имя каталога в каталоге файлов нашего приложения

Добавить компонент, содержащий FileProvider в сборку

Отредактировать файл android/build.gradle, добвив в секцию dependencies текст:

compile'com.android.support:support-v4:25.3.1'

секция целиком выглядит так:

dependencies {implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])compile'com.android.support:support-v4:25.3.1'}

Данная инструкция сработает, если был установлен Android Support Repository.

Настройка выполнена на основании статьи Sharing Files on Android or iOS from or with your Qt App - Part 4 и подобных статей на эту тему.

Заголовочный файл (.h) класса имеет вид:

#ifndef CAMSHOT_H#define CAMSHOT_H#include <QObject>#include <QImage>#include <QString>#include <QDebug>#include <QtAndroid>#include <QAndroidActivityResultReceiver>#include <QAndroidParcel>#include "auxfunc.h"class CamShot : public QObject, public QAndroidActivityResultReceiver{    Q_OBJECTpublic:    static const int RESULT_OK = -1;    static const int REQUEST_IMAGE_CAPTURE = 1;    static const int REQUEST_TAKE_PHOTO = REQUEST_IMAGE_CAPTURE;    enum ImgOrientation {ORIENTATION_UNDEFINED = 0, ORIENTATION_NORMAL = 1, ORIENTATION_FLIP_HORIZONTAL = 2, ORIENTATION_ROTATE_180 = 3, ORIENTATION_FLIP_VERTICAL = 4, ORIENTATION_TRANSPOSE = 5,                       ORIENTATION_ROTATE_90 = 6, ORIENTATION_TRANSVERSE = 7, ORIENTATION_ROTATE_270 = 8};    void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override;    static QImage aBitmapToQImage(const QAndroidJniObject &aBitmap);    static QImage camThumbnailToQImage(const QAndroidJniObject &data);    ImgOrientation needRotateAtRightAngle();    QImage camImageToQImage();    static void applyOrientation(QImage &img, const ImgOrientation &orientation);    explicit CamShot(QObject *parent = nullptr):QObject(parent),QAndroidActivityResultReceiver(){}    ~CamShot();private:        QAndroidJniObject tempImgURI;    QAndroidJniObject tempImgFile;    QAndroidJniObject tempImgAbsPath;    bool _thumbnailNotFullScaleRequested;public slots:    void aMakeShot(const bool &thumbnailNotFullScale = false);signals:    void createNew(const QImage &img);};#endif // CAMSHOT_H

Заголовочный файл (.cpp) класса имеет вид:

QImage CamShot::aBitmapToQImage(const QAndroidJniObject &aBitmap){    if (!aBitmap.isValid())        return QImage();    jint aBitmapWidth = aBitmap.callMethod<jint>("getWidth");    jint aBitmapHeight = aBitmap.callMethod<jint>("getHeight");    QAndroidJniEnvironment env;    const int32_t aBitmapPixelsCount = aBitmapWidth * aBitmapHeight;    jintArray pixels = env->NewIntArray(aBitmapPixelsCount);    jint aBitmapOffset = 0;    jint aBitmapStride = aBitmapWidth;    jint aBitmapX = 0;    jint aBitmapY = 0;    aBitmap.callMethod<void>("getPixels","([IIIIIII)V", pixels, aBitmapOffset, aBitmapStride, aBitmapX, aBitmapY, aBitmapWidth, aBitmapHeight);    jint *pPixels = env->GetIntArrayElements(pixels, nullptr);    QImage img(aBitmapWidth, aBitmapHeight, QImage::Format_ARGB32);    int lineSzB = aBitmapWidth * sizeof(jint);    for (int i = 0; i < aBitmapHeight; ++i){        uchar *pDst = img.scanLine(i);        const uchar *pSrc = reinterpret_cast<const uchar*>(pPixels + aBitmapWidth * i + aBitmapWidth);        memcpy(pDst, pSrc, lineSzB);    }    env->DeleteLocalRef(pixels); //env->ReleaseIntArrayElements(pixels, pPixels, 0); отвязывает указатель на данные массива от массива, а надо удалить сам массив, поэтому DeleteLocalRef.    return img;}QImage CamShot::camThumbnailToQImage(const QAndroidJniObject &data){    //Получить дополнительный данные    QAndroidJniObject bundle = data.callObjectMethod("getExtras","()Landroid/os/Bundle;");    qDebug()<<"bundle.isValid() "<<bundle.isValid()<<bundle.toString();    //Создать объект типа jstring (строка Java) со значением "data" - ключ для извлечения из ассоциативного контейнера пар <ключ, значение> миниатюры - объекта типа Bitmap (Java)    QAndroidJniObject bundleKey = QAndroidJniObject::fromString("data");    //Получить по ключу "data" дополнительный данные: миниатюру в виде объекта Bitmap    const QAndroidJniObject aBitmap (data.callObjectMethod("getParcelableExtra", "(Ljava/lang/String;)Landroid/os/Parcelable;", bundleKey.object<jstring>()));    qDebug()<<"aBitmap.isValid() "<<aBitmap.isValid()<<aBitmap.toString();    return aBitmapToQImage(aBitmap);}QImage CamShot::camImageToQImage(){    QAndroidJniObject bitmap = QAndroidJniObject::callStaticObjectMethod("android/graphics/BitmapFactory","decodeFile","(Ljava/lang/String;)Landroid/graphics/Bitmap;",tempImgAbsPath.object<jobject>());    qDebug()<<"bitmap.isValid() "<<bitmap.isValid()<<bitmap.toString();    QImage img = aBitmapToQImage(bitmap);    //Удаление файла    if (tempImgFile.isValid())        tempImgFile.callMethod<jboolean>("delete");    return img;}CamShot::ImgOrientation CamShot::needRotateAtRightAngle(){    //Вызов конструктора объекта    QAndroidJniObject exifInterface = QAndroidJniObject("android/media/ExifInterface","(Ljava/lang/String;)V",                                                     tempImgAbsPath.object<jstring>());    qDebug() << __FUNCTION__ << "exifInterface.isValid()=" << exifInterface.isValid();    QAndroidJniObject TAG_ORIENTATION = QAndroidJniObject::getStaticObjectField<jstring>("android/media/ExifInterface", "TAG_ORIENTATION");    qDebug() << __FUNCTION__ << "TAG_ORIENTATION.isValid()=" << TAG_ORIENTATION.isValid()<<TAG_ORIENTATION.toString();    const jint orientation = exifInterface.callMethod<jint>("getAttributeInt","(Ljava/lang/String;I)I",TAG_ORIENTATION.object<jstring>(),static_cast<jint>(ORIENTATION_UNDEFINED));    return static_cast<ImgOrientation>(orientation);}void CamShot::applyOrientation(QImage &img, const ImgOrientation &orientation){    switch (orientation){    case ORIENTATION_UNDEFINED:    case ORIENTATION_NORMAL:        break;    case ORIENTATION_FLIP_HORIZONTAL:{        img = img.mirrored(true, false);        break;    }    case ORIENTATION_ROTATE_180:        Aux::rotateImgCW180(img);        break;    case ORIENTATION_FLIP_VERTICAL:{        img = img.mirrored(false, true);        break;    }    case ORIENTATION_TRANSPOSE:{        img = img.mirrored(true, false);        Aux::rotateImgCW270(img);        break;    }    case ORIENTATION_ROTATE_90:        Aux::rotateImgCW90(img);        break;    case ORIENTATION_TRANSVERSE:{        img = img.mirrored(true, false);        Aux::rotateImgCW90(img);        break;    }        break;    case ORIENTATION_ROTATE_270:        Aux::rotateImgCW270(img);        break;    }}void CamShot::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data){    if ( receiverRequestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK )    {        if (_thumbnailNotFullScaleRequested){            const QImage thumbnail (camThumbnailToQImage(data));            if (!thumbnail.isNull())                emit createNew(thumbnail);            return;        }        const ImgOrientation orientation = needRotateAtRightAngle();        QImage image (camImageToQImage());        if (!image.isNull()){            applyOrientation(image, orientation);            emit createNew(image);        }    }}void CamShot::aMakeShot(const bool &thumbnailNotFullScale) {    QAndroidJniObject action = QAndroidJniObject::fromString("android.media.action.IMAGE_CAPTURE");    //Вызов конструктора объекта    QAndroidJniObject intent=QAndroidJniObject("android/content/Intent","(Ljava/lang/String;)V",                                                 action.object<jstring>());    qDebug() << __FUNCTION__ << "intent.isValid()=" << intent.isValid();    _thumbnailNotFullScaleRequested = thumbnailNotFullScale;    if (thumbnailNotFullScale) {        //Для получения миниатюры        QtAndroid::startActivity(intent, REQUEST_IMAGE_CAPTURE, this);        return;    }    //Для получения изображения в файл    QAndroidJniObject context = QtAndroid::androidContext();    QString contextStr (context.toString());    qDebug() <<"Context: "<<contextStr;    //Каталог для хранения файлов приложения    QAndroidJniObject extDir = context.callObjectMethod("getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;", NULL);    qDebug() << __FUNCTION__ << "extDir.isValid()=" << extDir.isValid()<<extDir.toString();    //Абсолютный путь к каталогу для хранения файлов приложения в виде строки    QAndroidJniObject extDirAbsPath = extDir.callObjectMethod("getAbsolutePath","()Ljava/lang/String;");    //Добавим имя каталога для совместного использования файлов этого приложения другими приложениями. См. /res/xml/file_paths.xml    extDirAbsPath = QAndroidJniObject::fromString(extDirAbsPath.toString() + "/shared");    const QString extDirAbsPathStr = extDirAbsPath.toString();    qDebug() << __FUNCTION__ << "extDirAbsPath.isValid()=" << extDirAbsPath.isValid()<<extDirAbsPathStr;    //Создать объект типа Файл для разделяемого каталога    QAndroidJniObject sharedFolder=QAndroidJniObject("java.io.File","(Ljava/lang/String;)V",                                                      extDirAbsPath.object<jstring>());    qDebug() << __FUNCTION__ << "sharedFolder.isValid()=" << sharedFolder.isValid()<<sharedFolder.toString();    const jboolean sharedFolderCreated = sharedFolder.callMethod<jboolean>("mkdirs");    Q_UNUSED(sharedFolderCreated);    //Прежде чем пытаться создать файл с заданным именем, нужно проверить файл с этим именем на существование    //Предположительно путь к этому файлу    QAndroidJniObject suggestedFilePath = QAndroidJniObject::fromString(extDirAbsPathStr+"/"+"_tmp.jpg");    qDebug() << __FUNCTION__ << "suggestedFilePath.isValid()=" << suggestedFilePath.isValid()<<suggestedFilePath.toString();    //Создать объект типа Файл    //Вызов конструктора объекта    QAndroidJniObject tempImgFile=QAndroidJniObject("java.io.File","(Ljava/lang/String;)V",                                                 suggestedFilePath.object<jstring>());    qDebug() << __FUNCTION__ << "fileExistsCheck.isValid()=" << tempImgFile.isValid()<<tempImgFile.toString();    //Удаление файла, если он существует    if (tempImgFile.isValid()){        const jboolean deleted = tempImgFile.callMethod<jboolean>("delete");        Q_UNUSED(deleted);    }    //Создать физический файл для записи в него изображения по указанному пути    const jboolean fileCreated = tempImgFile.callMethod<jboolean>("createNewFile");    Q_UNUSED(fileCreated);    //Абсолютный путь к созданному файлу в виде строки    tempImgAbsPath = tempImgFile.callObjectMethod("getAbsolutePath","()Ljava/lang/String;");    qDebug() << __FUNCTION__ << "tempImgAbsPath.isValid()=" << tempImgAbsPath.isValid()<<tempImgAbsPath.toString();    //Получить authority для fileprovider    const QString contextFileProviderStr ("org.qtproject.example.qsketch.fileprovider");    const char androidFileProvider  [] = "android/support/v4/content/FileProvider";    //const char androidxFileProvider [] = "androidx/core/content/FileProvider"; - не поддерживается Qt    /*QAndroidJniObject*/ tempImgURI = QAndroidJniObject::callStaticObjectMethod(androidFileProvider, "getUriForFile", "(Landroid/content/Context;Ljava/lang/String;Ljava/io/File;)Landroid/net/Uri;",                                                                             context.object<jobject>(), QAndroidJniObject::fromString(contextFileProviderStr).object<jstring>(), tempImgFile.object<jobject>());    qDebug() << __FUNCTION__ << "tempImgURI.isValid()=" << tempImgURI.isValid()<<tempImgURI.toString();    //Получить значение константы MediaStore.EXTRA_OUTPUT    QAndroidJniObject MediaStore__EXTRA_OUTPUT        = QAndroidJniObject::getStaticObjectField("android/provider/MediaStore", "EXTRA_OUTPUT", "Ljava/lang/String;");    qDebug() << "MediaStore__EXTRA_OUTPUT.isValid()=" << MediaStore__EXTRA_OUTPUT.isValid();    //Добавить URI путь файла для записи в него данных к задаче    intent.callObjectMethod("putExtra","(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;",MediaStore__EXTRA_OUTPUT.object<jstring>(), tempImgURI.object<jobject>());    qDebug() << __FUNCTION__ << "intent.isValid()=" << intent.isValid();    QtAndroid::startActivity(intent, REQUEST_IMAGE_CAPTURE, this);}

Так-же статические методы класса Aux для поворота изображения.

Заголовочный файл (.h) класса Aux имеет вид:

#ifndef AUXFUNC_H#define AUXFUNC_H#include <QImage>#include <QColor>#include <QPainter>#include <QMatrix>#include <QSize>#include <QPoint>class Aux{public:    static void resizeCenteredImg(QImage *image, const QSize &newSize, const QColor bgColor);    static void rotateImg(QImage &img, qreal degrees);    static void rotateImgCW90(QImage &img);    static void rotateImgCW180(QImage &img);    static void rotateImgCW270(QImage &img);};#endif // AUXFUNC_H

Файл исходного кода (.cpp) класса Aux имеет вид:

void Aux::resizeCenteredImg(QImage *image, const QSize &newSize, const QColor bgColor){    if (image->size() == newSize)        return;    const QSize szDiff = newSize - image->size();    QImage newImage(newSize, QImage::Format_ARGB32);    newImage.fill(bgColor);    QPainter painter(&newImage);    painter.drawImage(QPoint(szDiff.width()/2, szDiff.height()/2), *image);    *image = newImage;}void Aux::rotateImg(QImage &img, qreal degrees){    QPoint center = img.rect().center();    QMatrix matrix;    matrix.translate(center.x(), center.y());    matrix.rotate(degrees);    img = img.transformed(matrix, Qt::SmoothTransformation);}void Aux::rotateImgCW90(QImage &img){    const int w = img.width();    const int h = img.height();    const int maxDim = std::max(w, h);    resizeCenteredImg(&img, QSize(maxDim, maxDim), Qt::white);    rotateImg(img, 90);    resizeCenteredImg(&img, QSize(h, w), Qt::white);}void Aux::rotateImgCW180(QImage &img){    rotateImg(img, 180);}void Aux::rotateImgCW270(QImage &img){    const int w = img.width();    const int h = img.height();    const int maxDim = std::max(w, h);    resizeCenteredImg(&img, QSize(maxDim, maxDim), Qt::white);    rotateImg(img, 270);    resizeCenteredImg(&img, QSize(h, w), Qt::white);}

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

Результат зависит от переданного логического параметра thumbnailNotFullScale. Если он равен логической единице, то будет получена миниатюра, если логическому нулю, то полноразмерное изображение. Попытка получить миниатюру при запросе сохранения полноразмерного изображения в файл приведёт к исключению в JNI-вызовах.

Если миниатюра всегда ориентирована правильно, то полноразмерное изображение направлено в одну строну и его необходимо поворачивать. Информацию о необходимых преобразованиях можно получить из exif-свойств изображения при помощи ExifInterface. В обнаруженных в интернете Java-примерах преобразование к нормальной ориентации производится в Java-коде, в случае с Qt нет смысла мучить себя трудно отлаживаемыми, громоздкими JNI-вызовами и проще выполнить все необходимые преобразования в Qt.

Подробнее..

Как мы просто сократили объем входящего в дата-центр трафика на 70

03.02.2021 22:16:57 | Автор: admin

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

Единственное, о чем мы пожалели что не применили это решение раньше.

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

Два года назад, когда мы переходили с RedShift на ClickHouse, количество собираемых аналитических событий (приложение открылось, приложение запросило ленту контента, пользователь просмотрел контент, пользователь поставил смайл (лайк) и так далее) составляло около 5 млрд в сутки. Сегодня это число приближается к 14 млрд.

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

Но перед тем, как агрегировать, сохранить и обработать столько данных, их надо сначала принять и с этим есть свои проблемы. Часть описана в статье о переходе на ClickHouse (ссылка на неё была выше), но есть и другие.

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

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

Но ближе к лету непростого 2020 года ей нашлось применение.

Протокол HTTP, помимо сжатия ответов (о котором знают все, кто когда-либо оптимизировал скорость работы сайтов), позволяет использовать аналогичный механизм для сжатия тела POST/PUT-запросов, объявив об этом в заголовке Content-Encoding. В качестве входящего обратного прокси и балансировщика нагрузки мы используем nginx, проверенное и надёжное решение. Мы настолько были уверены, что он сумеет ко всему прочему ещё и на лету распаковать тело POST-запроса, что поначалу даже не поверили, что из коробки он этого не умеет. И нет, готовых модулей для этого тоже нет, надо было как-то решать проблему самостоятельно или использовать скрипт на Lua. Идея с Lua нам особенно не понравилась, зато это знание развязало руки в части выбора алгоритма компрессии.

Дело в том, что давно стандартизированные алгоритмы сжатия типа gzip, deflate или LZW были изобретены в 70-х годах XX века, когда каналы связи и носители были узким горлышком, и коэффициент сжатия был важнее, чем потраченное на сжатие время. Сегодня же в кармане каждого из нас лежит универсальный микрокомпьютер первой четверти XXI века, оборудованный подчас четырёх- и более ядерным процессором, способный на куда большее, а значит алгоритм можно выбрать более современный.

Выбор алгоритма

Требования к алгоритму были простыми:

  1. Высокая скорость сжатия. Мы не хотим, чтобы приложения тормозили из-за второстепенной функции.

  2. Небольшое потребление процессорной мощности. Не хотим, чтобы телефоны грелись в руках пользователей.

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

  4. Permissive лицензия.

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

В итоге остановились на алгоритме Zstandard, по следующим причинам:

  • Высокая скорость сжатия (на порядок больше, чем у zlib), заточенность на небольшие объёмы данных.

  • Хороший коэффициент сжатия при щадящем уровне потребления CPU.

  • За алгоритмом стоит Facebook, разрабатывавший его для себя.

  • Открытый исходный код, двойная лицензия GPLv2/BSD.

Когда мы увидели первым же в списке поддерживаемых языков JNI, интерфейс вызова нативного кода для JVM, доступный из Kotlin мы поняли, что это судьба. Ведь Kotlin является у нас основным языком разработки как на Android, так и бэкенде. Обёртка для Swift (наш основной язык разработки на iOS) завершила процесс выбора.

Решение на бэкенде

На стороне бэкенда задача была тривиальная: увидев заголовок Content-encoding: zstd, сервис должен получить поток, содержащий сжатое тело запроса, отправить его в декомпрессор zstd, и получить в ответ поток с распакованными данными. То есть буквально (используя JAX-RS container):

// Обёртка над Zstd JNIimport org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream;// ...if (  containerRequestContext    .getHeaders()    .getFirst("Content-Encoding")    .equals("zstd")) {  containerRequestContext    .setEntityStream(ZstdCompressorInputStream(      containerRequestContext.getEntityStream()    ))}

Решение на iOS

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

import Foundationimport ZSTDfinal class ZSTDRequestSerializer {    private let compressionLevel: Int32    init(compressionLevel: Int32) {        self.compressionLevel = compressionLevel    }    func requestBySerializing(request: URLRequest, parameters: [String: Any]?) throws -> URLRequest? {        guard let mutableRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else {            return nil        }        // ...        mutableRequest.addValue("zstd", forHTTPHeaderField: "Content-Encoding")        if let parameters = parameters {            let jsonData = try JSONSerialization.data(withJSONObject: parameters, options: [])            let processor = ZSTDProcessor(useContext: true)            let compressedData = try processor.compressBuffer(jsonData, compressionLevel: compressionLevel)            mutableRequest.httpBody = compressedData        }        return mutableRequest as URLRequest    }}

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

Впрочем, и снижение объёма трафика было не сильно заметно. Дождавшись, пока новая версия клиента раскатится пошире, мы врубили сжатие на 100% аудитории.

Результат нас, мягко говоря, удовлетворил:

График падения трафика на iOSГрафик падения трафика на iOS

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

То есть мы на четверть сократили весь объём.

Решение на Android

Воодушевлённые, мы запилили сжатие для второй платформы.

// Тут перехватываем отправку события через interceptor и подменяем оригинальный body на сжатый если это запрос к eventsoverride fun intercept(chain: Interceptor.Chain): Response {   val originalRequest = chain.request()   return if (originalRequest.url.toString()               .endsWith("/events")) {      val compressed = originalRequest.newBuilder()            .header("Content-Encoding", "zstd")            .method(originalRequest.method, zstd(originalRequest.body))            .build()      chain.proceed(compressed)   } else {      chain.proceed(chain.request())   }}// Метод сжатия, берет requestBody и возвращает сжатыйprivate fun zstd(requestBody: RequestBody?): RequestBody {   return object : RequestBody() {      override fun contentType(): MediaType? = requestBody?.contentType()      override fun contentLength(): Long = -1 //We don't know the compressed length in advance!      override fun writeTo(sink: BufferedSink) {         val buffer = Buffer()         requestBody?.writeTo(buffer)         sink.write(Zstd.compress(buffer.readByteArray(), compressLevel))      }   }}

И тут нас ждал шок:

График падения на AndroidГрафик падения на Android

Так как доля Android среди нашей аудитории больше, чем iOS, падение составило ещё 45%. Итого, если считать от исходного уровня, мы выиграли суммарно 70% от, напомню, всего входящего трафика в ДЦ.

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

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

Также стало видно, что наши опасения относительно батарейки не оправдались. Наоборот, потратив немного процессорной мощности телефона на сжатие данных, мы экономим намного больше электричества на передаче этих данных в эфир, как на Wi-Fi, так и по сотовой сети.

Два слова, что ещё можно улучшить

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

При этом коэффициент сжатия увеличивается от 10-15% на текстах до 50% на однообразных наборах строк, как у нас. А скорость сжатия даже несколько увеличивается при размере словаря порядка 16 килобайт. Это, конечно, уже не приведёт к такому впечатляющему результату, но всё равно будет приятно и полезно.

Подробнее..

Чего ждать от коробочных приложений?

04.02.2021 14:12:29 | Автор: admin

Всем привет, меня зовут Мария и я CEO & co-founder компании Технофабрика. Продолжаю делиться заметками из жизни команды разработки и сегодняшняя тема дня ожидания и реальность коробочных решений. Про масштабирование, кастомизацию, поддержку, доработку и вот это вот все.

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

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

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

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

Поймите же, в 90% случаев, когда вы пишите или заставляете кого-то писать код, он на самом деле, совершенно никому не нужен. Чем меньше вы плодите кода, тем лучше. Прекратите все писать свой код, сейчас же!

Про готовое

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

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

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

Так и с готовыми приложениями. Если вам нужно что-то непритязательное и прямщас, например, вы придумали небольшой стартап с полутора сотрудниками и вам нужно проверить свою идею не позже чем до конца месяца, такое решение в самый раз. Не нужно задумываться ни о качестве теста, ни о свежести ингредиентов. Ну не нобелевский обед же накрываем, ну. Да, может подглючивать, вылетать и не совсем идеально подпадать под ваши конкретные нужды. Но в качестве быстрого старта почему бы и нет? Why do we call it beta? Cuz its beta then nothing!

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

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

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

Стикеры Anastasea SeaСтикеры Anastasea Sea

И вот тут начинается ад. Причем как для вас, так и для тех, к кому вы обратитесь.

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

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

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

и кастомное

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

  1. Этот код написан специально под ваш бизнес

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

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

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

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

Да, ресторан дороже. Да, пицца дешевле. Но что именно сейчас предпочтительнее по масштабу, качеству и больше соответствует моменту решать вам.

Подробнее..

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

07.02.2021 20:15:43 | Автор: admin
В новом дайджесте уязвимости в Android, сокращение аналитического трафика и жидкие персонажи, AR-маски и страдания Senior-а, работа с привычками, лучшие издатели года и многое другое!



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

iOS

Apple выпускает бета-версию iOS 14.5 и бета-версию macOS 11.3 для разработчиков
Треть iOS-разработчиков неправильно описывает использование конфиденциальных данных
Ленивая навигация в SwiftUI
Интеграция SpriteKit в приложение
Как создать представление коллекции карт в стиле Revolut на iOS
Как масштабировать изображение внутри заголовка TableView
Обрабатываем корутины Kotlin Multiplatform в Swift Koru
Как начать машинное обучение с помощью Swift и TensorFlow
Составная архитектура одна из лучших архитектур для SwiftUI
Когда писать self в Swift
SwiftUI и Core Data: путь MVVM
Создание мобильного чата с использованием Realm
Субмодули для Xcode
MortyUI: GraphQL + SwiftUI
Wyler: запись экрана на iOS

Android

Как root-права и альтернативные прошивки делают ваш android смартфон уязвимым
Android Academy. Вы все пропустили! Но это не точно
Android Broadcast: новости #2
В 2020 году Google выплатил рекордные 6.7 млн долларов за поиск уязвимостей
Harmony OS оказалась Android
Telegram начинает конкурс для Android-разработчиков
7 распространенных ошибок, которые легко сделать с Android Fragment
Плохие расширения Kotlin
Моделирование состояния UI на Android
Android тогда и сейчас: навигация
Нарушение Null-Safety в Kotlin с помощью циклических ссылок
Масштабируемое изображение с Jetpack Compose
9 любимых расширений Android KTX
Можно ли доверять измерениям времени в Profiler?
Модуляризация приложений Android в 2021 году
Расширения Kotlin
GaugeProgressView: круговой индикатор для Android
Bouncy: отскок для RecyclerView

Разработка

Как мы просто сократили объем входящего в дата-центр трафика на 70%
Китайцы создали альтернативу Android и iOS на Ubuntu для смартфонов и планшетов
UI-элементы и жесты в мобильных приложениях
Как мы научили мессенджер ТамТам распознавать адреса в тексте
Жидкий персонаж на Unity 3D
Использование сервисов и обработка их результатов в Xamarin
Бильярд на Unity 3D
Обзор технологий трекинга: AR Маски
Envoy как универсальный сетевой примитив
Чего ждать от коробочных приложений?
Flutter ListView и ScrollPhysics: Детальный взгляд
Эффект дождя. Частицы в Unity 3D
Podlodka #201: End-to-end ML
Дизайн приложений: примеры для вдохновения #30
МВД хочет добавить в приложение определение номеров мошенников
Задачи с собеседований: футбол с одной монеткой
5 страданий Senior-разработчика
Kite запустил Team Server для автодополнения кода на предприятиях
7 самых известных или дорогих ошибок в программном обеспечении
Шаблоны проектирования: 5 самых известных
Яндекс открывает набор на летние стажировки
Mail.ru Group открывает набор на бесплатные курсы по программированию и автотестированию
Годовой отчет Liftoff о трендах мобильной рекламы и приобретения пользователей
Blue Chips экономическая стратегия для мобильных устройств
Как создать продуманный дизайн push-уведомлений
Фундаментальные принципы дизайна темной темы
AppDynamics представила решение для защиты приложений от киберугроз
Итоги Flutter Warsaw 2020
Вопрос на техническом интервью после которых я сразу отказываюсь
Эффект мерцания в Flutter
Condensation: распределенная база с безопасностью

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

Bold: фитнес для пожилых
make sense: О работе с Retention, эффективных триггерах и формировании привычек
Telegram обогнал TikTok и стал самым скачиваемым приложением в январе 2021
LOVEMOBILE #11: Аналитика в Estee Lauder
AppLovin покупает Adjust
Отчет Состояние дейтинга 2021
Cutback Coach: умеренное потребление алкоголя
App Annie назвала топ паблишеров года
Facebook тестирует уведомление пользователей об использовании данных в iOS
Новые правила Apple изменят мобильную рекламу навсегда. Разработчики узнали об этом в июне, но только 13% подготовились
Its a good choice: грамотная аллокация бюджета при привлечении новых пользователей. Кейс Rate & Goods и Rocket10
Как продвигать инди-приложения? Бюджетные способы и кейсы
Тенденции UI/UX-дизайна 2021 года и как заставить их работать на вас
Перестаньте спрашивать своих пользователей, чего они хотят

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

Bluetooth Low Energy: подробный гайд для начинающих. Соединения и сервисы
Курсы и книги для изучения data science c нуля
Собираем нейросети. Классификатор животных из мультфильмов. Без данных и за 5 минут. CLIP: Обучение без Обучения + код
Интернет вещей по-русски. Канальный уровень OpenUNB. Общие положения и адресация устройств
Клавиатура для обучения слепой печати бьет током при ошибках
Facebook разрешил загрузку в Oculus через App Lab
Azure Quantum открыли для разработчиков
ARKit и бизнес: как разработчики используют дополненную реальность в серьезных задачах
Предсказываем рост популярности GameStop в 20 строк кода
Определение звуков с помощью глубокого обучения
8 примеров использования машинного обучения в финансах и финтехе

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

Опыт разработки первой мобильной игры на Unity или как полностью перевернуть свою жизнь

16.02.2021 16:13:42 | Автор: admin

От кого и для кого

Доброго времени суток! Меня зовут Николай, и я хочу рассказать свою историю и поделиться своим небольшим опытом в разработке своей первой игры. С чего начинал и какие трудности пришлось преодолеть на пути разработки. Статья ориентирована на тех, кто начинает, думает начать или уже разрабатывает свою первую игру. Зачем? Потому что на стадии разработки своей первой игры, сам не однократно читал статьи о подобном опыте, после прочтения которых "наматывал сопли на кулак" и продолжал разработку дальше. От идеи до выпуска в магазин.

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

С чего все начиналось

Шел третий курс универа

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

Выбор направления

Появилась острая необходимость найти "дело", которое будет приносить удовольствие, не придется отрываться от современного мира на длительный срок и иметь финансовый достаток в перспективе сравнимый с моей по образованию профессией. Конец 4 курса универа и мой выбор пал на IT индустрию, а именно на python разработчика. Уделив 2 недели теории, в частности технической документации языка, я начал развивать логику и выполняя задачки каждый день на протяжении полугода, пока в конце декабря 2018 года не обнаружил геймдев.

А вот и Unity!

Выглядит комично или даже банально, но я повелся на клик-бэйт видео с подобным названием "Как сделать свою первую игру за 15 минут" или "Делаю крутую игру за 5 минут без регистрации и смс". Посмотрев данные материалы, в голове появилась мысль, выделить себе пару дней в своем графике, и утолить свое любопытство, установив данную среду разработки на свой компьютер. Потыкав разные кнопочки, и написав код методом "copy-paste", я пришел в неописуемый восторг! Моя творческая натура внутри меня ликовала. Ведь это было так приятно наблюдать за тем, что ты "сам" написал пару минут назад, сейчас заставляет кубик крутиться, перемещаться или менять цвет. Так уж вышло, что средой разработки установленной на мой компьютер оказалась Unity.

Почему Unity?

Он бесплатный, не такой сложный в освоении, большое сообщество и тонны ресурсов для самообучения, поэтому отлично подходит для начинающих разработчиков. Мобильный рынок заполнен проектами созданные на Unity. Даже такие крупные компании как Blizzard, Riot Games, CD Project RED выпустили всеми известные хиты как Hearthstone, Wild Rift и Gwent, используя эту платформу. Приняв волевое решение, я решил уйти в геймдев на пару с Unity.

Подготовка к разработке

Формирование идеи

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

  1. Проект не разрабатывать больше 2-х месяцев, иначе увеличивается вероятность потерять энтузиазм и он окажется на полке "потом доделаю" так не дойдя до выпуска;

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

Мой выбор

2Д мобильная аркада с сетевым режимом до 6 человек , рейтинговой системой и вознаграждением. Разработка, которой заняла отнюдь не 2 , а все "12 месяцев".

Аргументы "за":

  • Мне показалось заставлять двигаться объекты будет проще, чем те же 3Д;

  • Мобильный рынок огромен и его доля более половины всей игровой индустрии;

  • Писать сюжеты для игр я не умею, да и опыта в этом нет никакого, поэтому я решил сделать упор на веселье. А играть всегда веселее вместе! Поэтому сетевая;

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

Аргументы "против":

  • Игра уже становилась не так уж проста, как советовали более опытные коллеги;

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

Аргументы "за" были очень привлекательны и я решил рискнуть. Как говорится - "Чем чёрт не шутит" и "Была не была"!

Знакомство с Unity и его изучение

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

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

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

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

Как только я накидал определенный план действий и уже собирался начать делать свой первый будущей "шедевр", встал серьезный вопрос

Где я возьму картинки, музыку и остальные элементы для своей будущей игры? Ведь я совершенно не умею сам это создавать

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

А ты сам все это нарисовал? А музыку ты писал тоже сам?

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

Совет: Не чурайтесь использовать чужие наработки или шаблоны, которые продают или прибегать к работе фрилансеров! Это взаимосвязанная выгода! Конечному пользователю все равно, сами вы рисовали самолетик несколько часов или потратили 10$ на его покупку в магазине, ведь главное результат!

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

Совет: Отслеживайте скидки на продаваемые ассеты в различным магазинах, особенно под новый год! Можно приобрести кучу ассетов по выгодной цене со скидкой до 90% в такое время.

Непосредственная разработка

Первые шаги

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

На этом этапе моя игра имела следующий вид:

Главное начатьГлавное начать

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

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

От простого к сложному

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

Эх, как же сильно была переработана финальная версия интерфейсаЭх, как же сильно была переработана финальная версия интерфейса

Интерфес и меню

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

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

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

  • усталость

  • потеря интереса

  • неувереность в своих силах

  • все кажется адом и этому нет конца и края

Совет: Скажу то, что я прочитал когда сам проходил этот этап. НЕ СДАВАЙСЯ! Как бы не было сложно, ни в кое случае НЕ СДАВАЙСЯ и НИ ШАГУ НАЗАД! Дойдя до самого конца ты познаешь лавину экстаза и самоудовлетворения от того, что ты не бросил все! И разумеется бесценный опыт!!!

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

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

Концептуальные различие с финальной версией отстствуютКонцептуальные различие с финальной версией отстствуют

Оптимизация

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

Я начал оптимизировать свой проект, только спустя 8 месяцев разработки. Из-за чего оптимизировать надо было много и долго:

  • картинки

  • материалы

  • звук

  • шейдеры

  • настройки камеры, рендеринга

  • интерфейс

  • скрипты

Это заняло у меня еще не меньше двух недель.

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

Одна голова хорошо, а несколько лучше

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

Совет: Найдите так называемых "жертв", которых будете использовать в качестве тестировщиков среди ваших родственников, близких друзей и коллег по цеху. Брат, сестера, мама, папа, парень, девушка, друг, подруга, кто угодно. Дайте ему смартфон с игрой, посадите его рядом и просто наблюдайте, что ему не нравится, а что нравится. Главное - просто молчите и смотрите!

Реклама и внутриигровые покупки

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

Софта для рекламных интеграций имеется множетство, в том числе и от самой Unity, так называемая Unity Ads. Однако, мой выбор пал на Google AdMob. Почему не Unity Ads? Почитав обзоры, я узнал, что контент рекламы содержит казино, рулетки и ставки. Тут уже на вкус и цвет, как говорится, но я не хочу чтобы реклама была связана с подобного рода сервисами. Я использовал межстраничную и рекламу с вознаграждением.

Совет: Реклама с вознаграждением, намного лучше, ведь игрок сам нажимает на просмотр рекламы, чтобы получить какие-либо "плюшки" в игре. Разработчик и пользователь в плюсе!

Покупки в игре, я реализовал подобным образом:

Финальная версия игры

"12 месяцев" кропотливой работы , и финальная версия выглядит примерно так:

Меню игрыМеню игрыСетевой ГеймплейСетевой Геймплей

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

Совет: Тут необходимо открыть еще одно "второе" дыхание , к ранее уже открытым +100500

Публикация игры

Большим плюсом выбора Unity - кроссплатформенность, что позволяет один проект выпустить на всех желаемых платформах (Android, iOS,PC,WebGl и др). К моменту написания статьи игра была опубликована только для Android в Google Play Market, но не за горами ios в Apple Store.

Какие "подводные камни" имеются?

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

Так в чем же проблема и где те самые "подводные камни"?

Политика конфиденциальности

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

Совет: Не откладывайте на потом этот пункт, делайте его паралельно с публикацией!

Идентификатор клиента OAuth

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

Сайт игры

Это не является обязательным пунктом, но лучше сделать сайт, где будут размещены новости вашего проекта, а так же политика конфиденциальности и прочие материалы для ознакомления. Оказывается в 2021 году сделать легкий и простой сайт достаточно просто. С шаблонами для разработки сайтов в Word Press, не долго думая, я останавливаюсь на нем. Для сайта необходим хостинг и собственный домен. Взвесив все "за" и "против", решил потратить пару тысяч рублей на его аренду, сроком на 48 месяцев и не "париться". В сети огромное количество предложений, так что проблем с этим тоже не было. Пару часов уходит на его настройку, и еще пару часов на наполнение его контентом. И вот уже есть свой собственный сайт для игры!

Совет: Чтобы получить заветную галочку во вкладке Окно запроса доступа OAuth в Google Cloud Platforms, иметь сайт игры и свой домен , где так же будет размещена политика конфиденциальности - является обязательным пунктом!

Совет: Так же, если используете рекламу от Google Admob, то сайт тоже необходим. В корневую папку вашего сайта добавляется файл app-ads.txt. Это позволяет рекламодателям понять, какие источники объявлений имеют право продавать рекламный инвентарь. Если не пройти авторизацию, то доход с рекламы будет сильно снижен!

GDPR

Еще одно бюрократическое препятствие осталось, на пути для публикации. Если ваше приложение имеет рекламу, то она может быть персонализированной, а значит ваше приложение собирает данные пользователей, чтобы успешно показывать рекламу. GDPR- (General Data Protection Regulation) -этозакон, принятый Европейским Парламентом, который описывает правила защиты данных для граждан ЕС. Это значит,чтобы показывать персональну рекламу, необходимо перед первым запуском вашей игры, пользователь должен принять соглашение, что ознакомлен с политикой конфиденциальности вашего приложения, а так же прочитать в каких целях будет использоваться его персональные данные и дать согласие/отказаться на их обработку. Разумееется это распространяется на резидентов из стран ЕС.

После выполнения всех выше изложенных пунктов, мое приложение успешно опубликовано в Google Play Market и не знает никаких проблем.

Краткая выжимка советов

  • Изучите рынок, и определитесь с направлением и жанром игры. Главное не стройте в начале "наполеоновские"планы, которые могут и не реализоваться!

  • Распишите план действий и пытайтесь четко следовать ему, попутно внося небольшие правки. Старайтесь укладываться в установленные планом сроки.

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

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

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

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

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

  • Научитесь использовать на базовом уровне Git. Незаменимый помощник при разработке игры, чтобы контролировать внесенные изменения.

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

Заключение

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

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

Если я где-то ошибся и был не прав - буду рад правкам и пожеланиям в комментариях. Готов ответить на любые вопросы по данной теме.

Чтобы не было недопониманий на счет даты релиза.

Впервые игра была опубликована 2 декабря 2019 года, и это было 10 месяцев разработки. После я был вынужден отдать долг своей родине. Срочную службу в армии я нес до 2 декабря 2020. После демобилизации, я сразу продолжил разработку. И 4 февраля 2021, после "12 месяцев" разработки, я выпустил проект.

Если Вам интересно посмотреть на результат моей работы, то вы можете найти в Google Play Market.

Название игры - Starlake

Подробнее..

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-календаря. Если хочется получить напоминание со ссылкой на эфир на электронную почту, можно зарегистрироваться на таймпаде. Запись тоже будет опубликуем ссылку на неё в отдельном посте.

До встречи в онлайне!

Подробнее..

SafetyNet Attestation описание и реализация проверки на PHP

11.02.2021 20:09:20 | Автор: admin

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

После многочасовых поисков и скрупулёзного изучения официальной документации Google решил поделиться полученным опытом. Потому что, кроме официальной документации, я нашел только отрывочные описания частных примеров реализации на разных ЯП. И ни намека на комплексное объяснение особенностей проверки по SafetyNet на сервере.

Статья будет полезна разработчикам, которые хотят подробнее разобраться с технологией верификации устройств по протоколу SafetyNet Attestation. Для изучения описательной части не обязательно знать какой-либо язык программирования. Я сознательно убрал примеры кода, чтобы сфокусироваться именно на алгоритмах проверки. Сам пример реализации на PHP сформулирован в виде подключаемой через composer библиотеки и будет описан ниже.

Дисклеймер: материал в явном виде содержит перевод официальной документации от Google с разъяснениями и описанием особенностей реализации, с которыми я столкнулся.

О технологии

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

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

Что позволяет проверить технология:

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

  2. Что в процессе взаимодействия клиента и сервера нет больше никого, кроме вашего приложения и сервера.

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

В каких случаях механизм не применим или не имеет смысла:

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

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

  3. Если требуется детальное понимание статусов модификации системы, на которой работает мобильное приложение. В протокол заложен механизм однозначного определения модификации устройства. Он состоит из двух переменных: ctsProfileMatch и basicIntegrity. Об их назначении чуть ниже.

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

Схематично процесс проверки клиента можно представить в виде схемы:

Рассмотрим поэтапно процесс верификации устройств по протоколу:

  1. Инициация процесса проверки со стороны клиента.Отправка запроса от клиента на Backend на генерацию уникального идентификатора проверки (nonce) сессии. В процессе выполнения запроса на сервере генерируется ключ (nonce) сессии, сохраняется и передаётся на клиент для последующей проверки.

  2. Генерация JSW-токена на стороне удостоверяющего центра.Клиент, получив nonce, отправляет его на удостоверяющий центр вместе со служебной информацией. Затем в качестве ответа клиенту возвращается JWS, содержащий информацию о клиенте, время генерации токена, информацию о приложении (хеши сертификатов, которыми подписывается приложение в процессе публикации в Google Store), информацию о том, чем был подписан ответ (сигнатуру). О JWS, его структуре и прочих подробностях расскажу дальше в статье.

  3. Затем клиент передаёт JWS в неизменном виде на Backend для проверки. А на стороне сервера формируется ответ с информацией о результате прохождения аттестации.После получения статуса проверки JWS на стороне сервера обычно сохраняют факт проверки и, опираясь на него, влияют на функциональность положительным или негативным образом. Например, клиентам, не прошедшим аттестацию, можно отключить возможность писать комментарии, голосовать за рейтинг, совершать иные активности, которые могут повлиять на качество контента приложения или создавать угрозу и неудобство другим пользователям.

Описание процесса верификации на стороне сервера JWS от удостоверяющего центра

Документация Google в рамках тестирования на сервере предлагает организовать online-механизм верификации JWS, при котором с сервера приложения отправляется запрос с JWS на удостоверяющий сервис Google. А в ответе от сервиса Google содержится полный результат проверки JWS.

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

Далее расскажу обо всём алгоритме верификации JWS, в том числе о верификации самих сертификатов (проверке цепочки сертификатов).

Подробнее о JWS

JWS представляет собой три текстовых (base64 зашифрованных) выражения, разделенные точками (header.body.signature):

Например:

eyJhbGciOiJSUzI1NiIsICJ4NWMiOiBbInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4xIiwgInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4yIl19.ewogICJub25jZSI6ICJ2ZXJ5c2VjdXJlbm91bmNlIiwKICAidGltZXN0YW1wTXMiOiAxNTM5ODg4NjUzNTAzLAogICJhcGtQYWNrYWdlTmFtZSI6ICJ2ZXJ5Lmdvb2QuYXBwIiwKICAiYXBrRGlnZXN0U2hhMjU2IjogInh5eHl4eXh5eHl4eXh5eHl5eHl4eXg9IiwKICAiY3RzUHJvZmlsZU1hdGNoIjogdHJ1ZSwKICAiYXBrQ2VydGlmaWNhdGVEaWdlc3RTaGEyNTYiOiBbCiAgICAieHl4eXh5eHl4eXh5eHl4eXh5eD09PT09Lz0iCiAgXSwKICAiYmFzaWNJbnRlZ3JpdHkiOiB0cnVlCn0=.c2lnbmF0dXJl

В данном примере после расшифровки base64 получим:

Header :

json_decode(base64_decode(eyJhbGciOiJSUzI1NiIsICJ4NWMiOiBbInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4xIiwgInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4yIl19))={"alg":"RS256","x5c":["verysecurepublicsertchain1","verysecurepublicsertchain2"]}

Body:

json_decode(base64_decode(ewogICJub25jZSI6ICJ2ZXJ5c2VjdXJlbm91bmNlIiwKICAidGltZXN0YW1wTXMiOiAxNTM5ODg4NjUzNTAzLAogICJhcGtQYWNrYWdlTmFtZSI6ICJ2ZXJ5Lmdvb2QuYXBwIiwKICAiYXBrRGlnZXN0U2hhMjU2IjogInh5eHl4eXh5eHl4eXh5eHl5eHl4eXg9IiwKICAiY3RzUHJvZmlsZU1hdGNoIjogdHJ1ZSwKICAiYXBrQ2VydGlmaWNhdGVEaWdlc3RTaGEyNTYiOiBbCiAgICAieHl4eXh5eHl4eXh5eHl4eXh5eD09PT09Lz0iCiAgXSwKICAiYmFzaWNJbnRlZ3JpdHkiOiB0cnVlCn0=))={"nonce":"verysecurenounce","timestampMs":1539888653503,"apkPackageName":"very.good.app","apkDigestSha256":"xyxyxyxyxyxyxyxyyxyxyx=","ctsProfileMatch":true,"apkCertificateDigestSha256":["xyxyxyxyxyxyxyxyxyx=====/="],"basicIntegrity":true}

Signature

json_decode(base64_decode(c2lnbmF0dXJl))= signature

Остановимся на том, что именно содержится во всем JWS.

Header:

  • alg алгоритм, которым зашифрованы Header и Body JWS. Нужен для проверки сигнатуры.

  • x5c публичная часть сертификата (или цепочка сертификатов). Также нужен для проверки сигнатуры.

Body:

  • nonce произвольная строка полученная с сервера и сохранённая на нём же.

  • timestampMs время начала аттестации.

  • apkPackageName название приложения, которое запросило аттестацию.

  • apkDigestSha256 хеш подписи приложения, которое загружено в Google Play.

  • ctsProfileMatch флаг, показывающий прошло ли устройство пользователя верификацию в системе безопасности Google (основной и самый жёсткий критерий, по которому можно понять было ли устройство заручено и прошло ли оно сертификацию в Google).

  • apkCertificateDigestSha256 хеш сертификата (цепочки сертификатов), которыми подписано приложение в Google Play.

  • basicIntegrity более мягкий (по сравнению с ctsProfileMatch) критерий целостности установки.

Signature

Бинарная сигнатура, с помощью которой можно сделать заключение, что тело сообщения JWS было подписано с использованием сертификатов (цепочки сертификатов) указанных в Header, и с использованием известного нам приватного ключа. Ключевое позволяет понять, что в цепочке взаимодействия нет никого, кроме нас и удостоверяющего центра Google.

Проверка сертификатов

Перейдём к непосредственной проверки каждой части полученного JWS. Начнём с сертификатов и алгоритма шифрования:

1. Проверяем, что алгоритм, с помощью которого подписано тело, нами поддерживается:

[$checkMethod, $algorithm] = JWT::$supported_algs[$statement->getHeader()->getAlgorithm()];if ($checkMethod != 'openssl') {   throw new CheckSignatureException('Not supported algorithm function');}

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

private function extractAlgorithm(array $headers): string{   if (empty($headers['alg'])) {       throw new EmptyAlgorithmField('Empty alg field in headers');   }   return $headers['alg'];}private function extractCertificateChain(array $headers): X509{   if (empty($headers['x5c'])) {       throw new MissingCertificates('Missing certificates');   }   $x509 = new X509();   if ($x509->loadX509(array_shift($headers['x5c'])) === false) {       throw new CertificateLoadError('Failed to load certificate');   }   while ($textCertificate = array_shift($headers['x5c'])) {       if ($x509->loadCA($textCertificate) === false) {           throw new CertificateCALoadError('Failed to load certificate');       }   }   if ($x509->loadCA(RootGoogleCertService::rootCertificate()) === false) {       throw new RootCertificateError('Failed to load Root-CA certificate');   }   return $x509;}

3. Валидируем сигнатуру сертификата (цепочки сертификатов):

private function guardCertificateChain(StatementHeader $header): bool{   if (!$header->getCertificateChain()->validateSignature()) {       throw new CertificateChainError('Certificate chain signature is not valid');   }   return true;}

4. Сверяем hostname подписавшего сервера с сервером аттестации Google (ISSUINGHOSTNAME = 'attest.android.com'):

private function guardAttestHostname(StatementHeader $header): bool{   $commonNames = $header->getCertificateChain()->getDNProp('CN');   $issuingHostname = $commonNames[0] ?? null;   if ($issuingHostname !== self::ISSUING_HOSTNAME) {       throw new CertificateHostnameError(           'Certificate isn\'t issued for the hostname ' . self::ISSUING_HOSTNAME       );   }   return true;}

Верификация тела JWS

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

1. Проверка nounce.

Тут все просто. Распаковали JWS, получили в Body nonce и сверили с тем, что у нас сохранено на сервере:

private function guardNonce(Nonce $nonce, StatementBody $statementBody): bool{   $statementNonce = $statementBody->getNonce();   if (!$statementNonce->isEqual($nonce)) {       throw new WrongNonce('Invalid nonce');   }   return true;}

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

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

Есть два параметра, на основе которых можно принимать решение о надежности устройства: ctsProfileMatch и basicIntegrity. ctsProfileMatch более строгий критерий, он определяет сертифицировано ли устройство в Google Play и верифицировано ли устройство в сервисе проверки безопасности Google. basicIntegrity определяет, что устройство не было скомпрометировано.

private function guardDeviceIsNotRooted(StatementBody $statementBody): bool{   $ctsProfileMatch = $statementBody->getCtsProfileMatch();   $basicIntegrity = $statementBody->getBasicIntegrity();   if (empty($ctsProfileMatch) || !$ctsProfileMatch) {       throw new ProfileMatchFieldError('Device is rooted');   }   if (empty($basicIntegrity) || !$basicIntegrity) {       throw new BasicIntegrityFieldError('Device can be rooted');   }   return true;}

3. Проверяем время начала прохождения аттестации.

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

private function guardTimestamp(StatementBody $statementBody): bool{   $timestampDiff = $this->config->getTimeStampDiffInterval();   $timestampMs = $statementBody->getTimestampMs();   if (abs(microtime(true) * 1000 - $timestampMs) > $timestampDiff) {       throw new TimestampFieldError('TimestampMS and the current time is more than ' . $timestampDiff . ' MS');   }   return true;}

4. Проверяем подпись приложения.

Здесь тоже два параметра: apkDigestSha256 и apkCertificateDigestSha256. Но apkDigestSha256 самой Google помечен как нерекомендуемый способ проверки. С марта 2018 года они начали добавлять мета-информацию в приложения из-за чего ваш хеш подписи приложения может не сходиться с тем, который будет приходить в JWS (подробнее здесь).

Поэтому единственным способом проверки остается проверка хеша подписи приложения apkCertificateDigestSha256. Фактически этот параметр нужно сравнить с теми sha1 ключа, которым подписываете apk при загрузке в Google Play.

private function guardApkCertificateDigestSha256(StatementBody $statementBody): bool{   $apkCertificateDigestSha256 = $this->config->getApkCertificateDigestSha256();   $testApkCertificateDigestSha256 = $statementBody->getApkCertificateDigestSha256();   if (empty($testApkCertificateDigestSha256)) {       throw new ApkDigestShaError('Empty apkCertificateDigestSha256 field');   }   $configSha256 = [];   foreach ($apkCertificateDigestSha256 as $sha256) {       $configSha256[] = base64_encode(hex2bin($sha256));   }   foreach ($testApkCertificateDigestSha256 as $digestSha) {       if (in_array($digestSha, $configSha256)) {           return true;       }   }   throw new ApkDigestShaError('apkCertificateDigestSha256 is not valid');}

5. Проверяем имя приложения, запросившего аттестацию.

Сверяем название приложения в JWS с известным названием нашего приложения.

private function guardApkPackageName(StatementBody $statementBody): bool{   $apkPackageName = $this->config->getApkPackageName();   $testApkPackageName = $statementBody->getApkPackageName();   if (empty($testApkPackageName)) {       throw new ApkNameError('Empty apkPackageName field');   }   if (!in_array($testApkPackageName, $apkPackageName)) {       throw new ApkNameError('apkPackageName ' . $testApkPackageName. ' not equal ' . join(", ", $apkPackageName));   }   return true;}

Верификация сигнатуры

Здесь нужно совершить одно действие, которое даст нам понимание того, что Header и Body ответа JWS подписаны сервером авторизации Google. Для этого в исходном виде склеиваем Header c Body (с разделителем в виде ".") и проверяем сигнатуру:

protected function guardSignature(Statement $statement): bool{   $jwsHeaders = $statement->getRawHeaders();   $jwsBody = $statement->getRawBody();   $signData = $jwsHeaders . '.' . $jwsBody;   $stringPublicKey = (string)$statement->getHeader()->getCertificateChain()->getPublicKey();   [$checkMethod, $algorithm] = JWT::$supported_algs[$statement->getHeader()->getAlgorithm()];   if ($checkMethod != 'openssl') {       throw new CheckSignatureException('Not supported algorithm function');   }   if (openssl_verify($signData, $statement->getSignature(), $stringPublicKey, $algorithm) < 1) {       throw new CheckSignatureException('Signature is invalid');   }   return true;}

Вместо заключения. Библиотека на PHP

Уже после решения задачи и отдельно от нашей кодовой базы, я разработал библиотеку на PHP, которая обеспечивает полный цикл верификации JWS.

Её можно скачать из Packagist и использовать в своих прое

Подробнее..

Темы, стили и атрибуты

20.02.2021 18:04:03 | Автор: admin

В Android существуют стили и темы которые позволяют структурировать разработку пользовательского интерфейса.

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

Пример объявления атрибута из Android SDK:

<attr name="background" format="reference|color" />

Примечание:

Ссылка на другой атрибут через @[package:]type/name структуру тоже является типом.

Темы vs стили

Темы и стили очень похожи, но используются для разных целей.

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

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

Стили и темы предназначены для совместной работы.

Например, у нас есть стиль, в котором фон кнопки - colorPrimary, а цвет текста - colorSecondary. Фактические значения этих цветов приведены в теме.

<?xml version="1.0" encoding="utf-8" ?><resources xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"><style name="LightTheme" parent="YourAppTheme"><item name="colorPrimary">#FFFFFF</item><item name="colorSecondary">#000000</item></style><style name="DarkTheme" parent="YourAppTheme"><item name="colorPrimary">#000000</item><item name="colorSecondary">#FFFFFF</item></style><style name="Button.Primary" parent="Widget.MaterialComponents.Button"><item name="android:background">?attr/colorPrimary</item><item name="android:textColor">?attr/colorSecondary</item></style></resources>

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

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

Виды ссылок в XML

Атрибут android:background может принимать несколько типов:

android:background="@color/colorPrimary"android:background="?attr/colorPrimary"

В случае с @color/colorPrimary - мы cсылаемся на цветовой ресурс colorPrimary, а точнее на <color name="colorPrimary">#FFFFFF<color> строку, которая прописана в res/values/color.xml файле.

Примечание:

Цвет - это ресурс, на который ссылаются используя значение, указанное в атрибуте name, а не имя XML-файла. Таким образом, можно комбинировать цветовые ресурсы с другими ресурсами в XML-файле под одним элементом <resources>, но я этого не рекомендую.

В свою очередь, ?attr - это ссылка на аттрибут темы.

?attr/colorPrimary указывает на colorPrimary атрибут, который находится в текущей теме:

<?xml version="1.0" encoding="utf-8" ?><resources xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"><style name="YourAppTheme" parent="Theme.AppCompat.Light.NoActionBar"><item name="colorPrimary">@color/colorPrimary</item></style></resources>

Преимущество ?attr ссылок в том, что они будут меняться в зависимости от выбранной темы.

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

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

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

Структура ссылок

@[package:]type/name

  1. package - (опционально) название пакета в котором находиться ресурс. По умолчанию - это пакет приложения, в котором находится ресурс.

  2. type - может быть одним из color, string, dimen, layout или какого-либо другого типа ресурса. Более подробно читайте здесь.

  3. name - имя ресурса, используется как идентификатор ресурса.

?[package:]type/name

  1. package - (опционально) название пакета в котором находиться ресурс. По умолчанию - это пакет приложения, в котором находится ресурс.

  2. type - (опционально) всегда attr когда используем ?.

  3. name - имя ресурса, используется как идентификатор ресурса.

? vs ?attr vs ?android:attr

Возможно, вы замечали, что к некоторым атрибутам можно обратиться как ?android:attr/colorPrimary, так и ?attr/colorPrimary, а также ?colorPrimary.

Это связано с тем, что некоторые атрибуты определены в Android SDK, и поэтому нужно указывать приставку android, чтобы ссылаться на них.

Мы можем использовать ? и ?attr в случае, когда эти атрибуты находятся в библиотеках(Например, в AppCompat или MaterialDesign), которые компилируюся в приложение, поэтому пространство имен не требуется.

Некоторые элементы определены и в Android SDK, и в библиотеке, например colorPrimary.

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

Полезные ссылки

Ниже список с интересными статьями о стилях и темах от Google Android разработчиков:

  1. Theming with AppCompat

  2. Android styling: themes vs styles

  3. Whats your texts appearance?

  4. Android Styling: themes overlay

  5. Android Styling: prefer theme attributes

  6. Android styling: common theme attributes

Рекомендую ещё к просмотру видео с Android Dev Summit 2019 года. Ссылка на видео

Подробнее..

Работа с FTP протоколом в Android. Пример

21.02.2021 00:06:08 | Автор: admin
Всем привет! Это будет очень маленькая статья. Наша задача тут: подключиться к локальному серверу FTP (я выбрала FileZilla) и отправить туда чего-нибудь используя (очевидно) FTP протокол.
Итак, капелька теории:
FTP (File Transfer Protocol) протокол передачи файлов по сети.
Для работы по FTP нужны двое: FTP-сервер и FTP-клиент.
Сервер обеспечивает доступ по логину и паролю к нужным файлам и, соответственно, показывает пользователю только те файлы и папки, к которым он имеет доступ.
Клиент программа для FTP-соединения. Основная идея в том, чтобы пользователь мог оперировать файловыми данными на стороне сервера: просматривать, редактировать, копировать, загружать и удалять.
Все логично.
В отличие, скажем, от того же HTTP FTP использует в качестве модели соединения двойное подключение. При этом один канал является управляющим, через который поступают команды серверу и возвращаются его ответы (обычно через TCP-порт 21), а через остальные происходит собственно передача данных, по одному каналу на каждую передачу. Поэтому в рамках одной сессии по протоколу FTP можно передавать одновременно несколько файлов, причём в обоих направлениях.

Если расписать процесс установки соединения и дальнейшей передачи данных по пунктам, то получится примерно следующее:

  1. Пользователь активирует клиентское приложение и соединяется с сервером, введя логин и пароль.
  2. Устанавливается управляющее соединение между соответствующими модулями интерпретаторами протокола со стороны клиента и сервера.
  3. Пользователь посредством клиента посылает команды серверу, определяющие различные параметры FTP-соединения (активный или пассивный режим, FTP порт, вид передачи данных, их тип), а также директивы для действий, которые юзер намерен осуществить (например, удалить, переименовать, закачать файл и т.д.).
  4. Далее один из участников (к примеру, клиент), являющийся пассивным, становится в режим ожидания открытия соединения на FPT порт, который задан для передачи информации.
    Затем активный участник открывает соединение и начинает передавать данные по предназначенному для этого каналу.
  5. По завершении передачи, это соединение закрывается, но управляющий канал между интерпретаторами остается открытым, вследствие чего пользователь в рамках той же сессии может вновь открыть передачу данных.

FTP соединение по умолчанию происходит через порт 21, если не установлен другой порт.

image

Реализация
В данном случае будем использовать класс FTPClient из библиотеки Apache (org.apache.commons.net.ftp.FTPClient). Создаем объект класса, устанавливаем таймаут. Затем мы должны сначала подключиться к серверу с помощью connect, прежде чем что-либо делать (и не забыть отключиться после того, как все сделаем). В connect в качестве первого параметра пишем локальный адрес хоста, вторым аргументом порт для подключения. Также необходимо проверить код ответа FTP (слишком очевидно, но все же), чтобы убедиться, что соединение было успешным.

import org.apache.commons.net.ftp.FTPimport org.apache.commons.net.ftp.FTPClientimport org.apache.commons.net.ftp.FTPReply...val con = FTPClient()        con.connectTimeout = ххх        con.connect("192.168.0.105", 21)        if (FTPReply.isPositiveCompletion(con.replyCode)) {                //все круто        }

Хорошо, мы подключились к серверу. Что дальше? Теперь необходимо пройти авторизацию под именем пользователя, которого мы, собственно, создали для этого. Но перед этим нужно установить для текущего режима подключения к данным значение PASSIVE_LOCAL_DATA_CONNECTION_MODE с помощью команды enterLocalPassiveMode(). Это значит, что все передачи будут происходить между клиентом и сервером, и что сервер находится в пассивном режиме, ожидая подключения клиента для инициализации передачи данных. (например, ACTIVE_LOCAL_DATA_CONNECTION_MODE подразумевает, что инициатором подключения будет выступать сервер).
После в login пишем логин и пароль пользователя. storeFile() сохраняет файл на сервере, используя заданное имя и принимая входные данные из заданного InputStream. У меня была задача периодически писать в определенный файл на сервере, поэтому это выглядит так:

con.enterLocalPassiveMode()                if (con.login("nad", "nad")) { //вводим логин и пароль (да, вот так в открытом виде)                    con.setFileType(FTP.BINARY_FILE_TYPE)                    val msg = "your_msg"                    val msg_bytes: InputStream = ByteArrayInputStream(msg.toByteArray())                    val result: Boolean = con.storeFile("/sms.txt", msg_bytes)                    msg_bytes.close()                    if (result) Log.v("upload result", "succeeded")                    con.logout()                    con.disconnect()                }            }             catch (ex: IOException) {                ex.printStackTrace()                return Result.failure()            }

Если же вы хотите скопировать свой файл на сервер целиком, можно сделать так (суть не особо меняется, но вдруг кому-то нужно):

val file = File("путь до файла")val msg_bytes = FileInputStream(file)


В целом это все, что вам может понадобиться. И, конечно, немного о настройке локального FTP сервера. У меня это, как и писала ранее, FileZilla. Все оставляем по дефолту. Создаем пользователя и присваиваем ему папку на хосте, из которой он может читать/писать/удалять и т.д.

image

Вот и все. Запускаем и смотрим логи на сервере. И вот чего должны получить:

image

Удачи!
Подробнее..

Помощь многим Android-приложение для людей с особыми потребностями

26.02.2021 20:18:58 | Автор: admin

Приветствую всех! Я Беглецов Глеб, учусь в 11 классе, летом прошлого года закончил программу IT Школа Samsung в г. Санкт-Петербург на площадке ФМЛ 239 под руководством Левина Михаила Константиновича. В качестве выпускной работы я разработал приложение, которое назвал Parus. Это мой первый большой проект под Android, и он мне принес ГРАН-ПРИ финала Всероссийского конкурса IT Школы Samsung (ролик). Хочу поделиться историей создания этого проекта.

Идея

Я иногда слышал среди своих одногруппников в IT Школе Samsung подобную фразу: Я не знаю, какое приложение мне написать. У меня не было сомнений. Моё приложение должно помочь людям с особыми потребностями полноценно общаться с миром, потому что данная проблема актуальна и в нашей стране, и в мире. И меня она лично касается. Есть много отдельных приложений: для синтеза речи, для искусственного зрения, для напоминаний и т.д. Мне хотелось объединить всё необходимое в одном приложении.

Начало

Учиться в IT Школе Samsung я начал в сентябре, а в начале ноября уже знал, какой проект буду разрабатывать. Но это же классика, откладывать всё на последний момент! В итоге апрель и май выдались очень напряженными. В тоже время это было очень увлекательное время. Я просыпался, кодил с перерывами на еду, ложился спать, просыпался и снова кодил, если не учился в школе. Вот такой был распорядок дня в течение двух завершающих месяцев обучения. Узнавал каждый день что-то новое. Несколько раз было такое: понимал, что написанное не годится и переписывал половину уже сделанной работы. Тогда, как понимаете, было не очень весело.

К началу апреля я освоил основы Java и Android. А за следующие два месяца добавил синтез речи, распознавание речи, компьютерное зрение, отслеживание пульса, создание ежедневных напоминаний (не todo list), связь аккаунтов человека с особыми потребностями и его помощником, чат и передачу данных между ними. Дальше подробнее про каждую функцию.

Говорить. Синтез речи

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

Создавая систему коллекций и частых фраз, я не знал о существовании Snapshotов (обновление данных в реальном времени), поэтому обновление данных у меня было реализовано, как я позже понял, через очень шаткие костыли. Я читал данные из БД каждый раз, когда там, гипотетически, могли измениться какие-либо поля. И такая система была реализована ВЕЗДЕ. Вот это и был один из тех моментов, когда, узнав про что-то новое и полезное, обрадовался, а затем взялся за переписывание всего кода.

Смотреть

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

Вообще, при реализации многих задач мне очень помог Firebase, отличный инструмент. Для компьютерного зрения я поначалу попробовал использовать Tesseract, потому что была возможность подключить к нему файл с русским алфавитом для распознавания кириллицы. Оказалось зря. Потратив только на его установку в проект около недели, текст с русскими буквами так и не распознавался. Поэтому я стал искать другие варианты. И тут меня спас Firebase Ml Kit, который я настроил в 5 раз быстрей! Firebase даёт очень много плюшек, и без него я вряд ли бы всё сделал. По неопытности, конечно, не сразу все получалось, но я разобрался. Для текущей задачи пока я использую API от Firebase. Может, в будущем найду что-то более подходящее.

Слушать

Просто распознавание речи с помощью SpeechRecognizer API от Google и вывод результата на экран.

Пульс. Google Fit vs Samsung Health SDK

Отслеживание пульса полезная функция для людей с сердечными заболеваниями. С помощью неё пользователь может следить за своим сердцебиением. При обработке данных, если пульс вне нормального интервала (55 < x < 90), то на устройство приходит уведомление об этом.

Но над этой задачей тоже пришлось попыхтеть. Сначала разобрался, как читать данные из Google Fit. Потом прочитал про Samsung Health SDK и решил перейти на него. У S Health SDK есть очень удобное дополнение Data Viewer. С его помощью можно записывать данные в S Health без каких-либо трекеров. Это очень помогает при тестировании приложения, в этом SDK от Samsung выигрывает у GoogleFit. Также не надо делать привязку к аккаунту и подключать QAuth от Google, тоже плюс.

Но если Google Fit может встроить в свой проект любой желающий, то для использования Samsung Health необходимо, чтобы приложение получило статус партнёрского. Без такого статуса приложение будет полноценно работать только в режиме разработчика. К сожалению, на данный момент приостановлен приём заявок на статус партнёрского приложения в связи с обновлением партнёрской программы. Поэтому я был вынужден вернуться обратно к GoogleFit.

Напоминания

Я знаю, что существует множество приложений по типу todo list, но, как говорится, это другое Во-первых, пользователь может установить несколько часов (12:50, 13:10 и 18:30, например) на одну задачу. Во-вторых, есть возможность установить напоминанию интервал времени и повторение: с 12:00 до 20:00 каждый час, например. Напоминания каждый день перезагружаются и начинают всё по новой, как обычный будильник. Это может быть полезно людям, которым надо каждый день сделать что-то важное. Например, люди с диабетом могут установить время для инсулиновых уколов.

Безусловно, что человек может установить множество будильников, которые будут выполнять ту же задачу. Моя программа просто упрощает это. Для напоминаний с 9:00 до 21:00 с регулярностью в полчаса, необходимо установить либо 24 обычных будильника, либо 1 напоминание с интервалом в Parus. Думаю, что второй вариант легче.

Напоминания не сильно тратят заряд батареи, потому что они реализованы за счёт WorkManagerов. Я долго думал между Alarm и WorkManager, потому что первое слишком сильно нагружает телефон и батарею (+на некоторых телефонах ограничен), а второе имеет один весомый минус из-за сверхоптимизации напоминание может не отобразиться, если телефон долго находился в режиме сна. Я около 100 раз протестил данный случай, и 3-5 раз напоминание не срабатывало до выхода из режима сна. На сайте Android developers читал, что Google работает над этим, так что буду надеяться, что скоро это исправят.

Связь с помощником

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

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

  • Кнопка вызова
    При связывании аккаунтов, у человека с особыми потребностями появляется быстрая кнопка Позвать помощника. При нажатии на неё на устройство помощнику отправляется уведомление.

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

Samsung Android Bootcamp. Заключение

Выше я описал основные возможности своего приложения на момент участия в конкурсе выпускных проектов IT Школы Samsung. Я совсем не упоминал архитектуру приложения или паттерны, которые я использовал. Знаете почему? Потому что этого не было на момент финала конкурса. Проект состоял из классов, разложенных по пакетам. И всё. Я подозревал, что это ужасно, но не знал, как исправить. И тут нам сказали, что сразу после конкурса пройдет летний Android Bootcamp для выпускников IT Школы Samsung. Я решил, что мне явно туда.

За две недели интенсива я узнал безумно много полезных вещей. Например, зацепился за MVVM + Data Binding + Material Design. Стало понятно, что Parus выпускать в таком виде нельзя. Предстоящая работа: сначала архитектура, потом дизайн, после чего можно и в Google Play. В августе уже не было такого запала, как в конце весны, к середине сентября я почти переделал всё под MVVM архитектуру и все

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

Прошел Новый год. Parusa так и нет в Google Play. Я открыл Android Studio и вдруг почувствовал, что есть силы завершить проект. Я дал себе обещание, что в феврале Parus появится в Play Market. За две недели доделал всё, что планировал, и 4 февраля загрузил проект в Google Play.

Вот такая история моего первого большого проекта. Да, Parus не идеален, но у меня получилось довести его до законченного состояния. Надеюсь, что он окажется полезным кому-нибудь. Буду рад, если вы скачаете приложение и напишете свои предложения. Особенно, если вы моя целевая аудитория - человек с особыми потребностями или заботитесь о таком человеке.

Спасибо, что прочитали статью до конца.

Пока!

Глеб Беглецов

Ученик 11 класса, школа Озерки

г. Санкт-Петербург

Подробнее..

Как увеличить срок хранения мобильного приложения? 6 проверенных способов

28.02.2021 02:15:32 | Автор: admin

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

1. Специальные акции для пользователей

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

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

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

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

2. Интеллектуальные push-уведомления

Данные о поведении и геолокации, часто получаемые в режиме реального времени, позволяют точно связаться с людьми с сообщением в нужном месте и времени (например, Здравствуйте! Вы чувствуете себя сонным? Сегодня со скидкой [здесь стоимость акции]). Это дает им преимущество перед, например, ремаркетингом на основе файлов cookie. Придавая правильную ценность пользователю через персонализированное сообщение, мы увеличиваем вероятность совершения покупки.

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

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

Подробно про это я уже писал в другой своей статье. Если будет интересно - выложу на хабре.

3. Индексация приложений, то есть индексация приложений в поисковой системе.

В 2015 году, после трех лет тестирования, Google, наконец, решил ввести функцию индексации приложений в поисковой системе общего пользования (избранные разработчики получили доступ к ней двумя годами ранее; именно их приложения проложили путь для преемников). В настоящее время Google индексирует приложения для мобильных устройств с iOS и Android, что означает, прежде всего, гораздо больше возможностей для охвата получателей. Индексируя приложение, потенциальные пользователи могут найти его в результатах поиска Google. Я говорю здесь - конечно - о людях, просматривающих интернет с телефонов и планшетов. Если у них есть конкретное приложение, нажимающее на ссылку (например, размещенную на веб-сайте), они автоматически переносятся на один из его экранов (например, ответы на конкретный вопрос или товар). Если нет - они могут установить его, нажав кнопку на странице результатов (или в мобильной версии сайта). В этой ситуации они переносятся прямо в App Store или платформу Google Play.

Следует сразу отметить, что Google индексирует только те приложения, которые соответствуют условиям. Программная документация о так называемой индексация приложений доступна на веб-сайтеFirebase App Indexing. Внедрение функций индексации (например, глубоких ссылок с мобильной страницы на экраны приложений) должно осуществляться на этапе разработки программы и являться частью более широкого плана присутствия в Интернете. Сам процесс реализации индексации приложений варьируется в зависимости от операционной системы (мы делаем по-разному в случае iOS, по-разному - Android) и подробно описанздесь (iOS)издесь (Android).

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

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

Принцип роботы "глубоких ссылок" или deep link

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

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

5. Google UAC - универсальные кампании по продвижению приложений

Кампании попродвижению универсальных приложений(Universal App Campaigns, корочеUAC) - хороший вариант для людей, которые хотят автоматизировать рекламные процессы; После загрузки всех необходимых материалов в AdWords система распространения рекламы будет решать, кто, когда, что и как отображать, чтобы достичь нашей цели.

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

Вы можете узнать больше о кампаниях Universal App наофициальной странице справки Google. Там мы также найдем информацию о различных способах установления целей, ориентированных на удержание.

6. Увеличение удержания мобильного приложения за пределы цифрового маркетинга: ATL и BTL

Продвижение мобильного приложения не должно ограничиваться интернет-маркетингом. Продвижение ATL (выше линии), например, рекламные щиты, газеты, листовки и BTL (ниже линии), например, городские игры в среде дополненной реальности и POSy, при правильном использовании они также могут хорошо работать.

Резюмируя

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

Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru