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

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

Код ваш, призы наши принимаем заявки на онлайн-хакатон ВТБ More.Tech

25.09.2020 18:15:34 | Автор: admin
Привет! Мы начали приём заявок на ВТБ More.Tech онлайн-хакатон для молодых амбициозных айтишников. От вас профессиональные навыки, желание участвовать в web- или mobile-треках соревнования и умение работать в команде. От нас призовой фонд 900 тыс. рублей и возможность начать карьеру в системообразующем российском банке.

image

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



Первый трек хакатона для специалистов в web-разработке, второй для разработчиков под iOS/Android. ВТБ More.Tech командное состязание, поэтому лучше заранее собрать под свои знамёна коллег-единомышленников. Впрочем, мы без проблем принимаем заявки от отдельных участников, из которых в дальнейшем сформируются команды.

Задача web-трека создание антифрод-системы для web-приложений банков. Оптимальный, на наш взгляд, состав команды для защиты от мошенников включает frontend- и backend-разработчиков, DevOps-инженера, системного аналитика и product/project-менеджера.

Участникам mobile-трека предстоит разработка приложения, которое сможет распознать модель и марку автомобиля, подобрать актуальное предложение для его продажи и сформировать кредитную заявку. Помимо мобильного и backend-разработчиков mobile-команде явно не помешают дизайнер, а также специалист по компьютерному зрению. И product/project-менеджер, куда же без него.

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

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

Проанализировав опыт крупнейших хакатонов, в том числе знаменитый Tech Fest Munich, мы решили выстроить работу над конкурсными проектами по принципу Dive-Create-Impact: 15 % времени на изучение условий задачи и мозговой штурм, 60 % непосредственно на реализацию, 25 % на доработку и создание презентации. Мы считаем, что такой тайминг способствует принятию наиболее эффективных решений. К тому же участники смогут освоить этот подход и использовать его в своей дальнейшей работе.

Несмотря на режим удалёнки, все четыре дня хакатона насыщены активностями, при этом деятельность каждой команды будут курировать наши менторы. В программе серия митапов с экспертами ВТБ (методология DevOps, гибкие технологии/Agile и другие темы) и мастер-класс по подготовке презентаций. Ознакомиться с расписанием, а также почитать о задачах и условиях участия подробнее можно на официальном сайте хакатона. Там же вы найдёте форму для подачи заявки. Решайтесь скорее: последний день приёма заявок 4 октября.
Подробнее..

PWA не для всех

20.09.2020 22:11:04 | Автор: admin

В комментах к моей предыдущей статье о service worker'ах была высказана мысль, что PWA на десктопах - вещь малополезная. Примерно полгода назад я разбирался с тем, как прикрутить PWA Vue Storefront к магазинам на платформе Magento и мне понравилось, как шустро крутилось в моём компьютере это приложение по сравнению с оригинальным web-интерфейсом. Мой персональный опыт показывал, что PWA на десктопах имеет неплохую перспективу, но дальнейшее углубление в тему показало, что коллега @sumanai со своим отрицанием PWA на десктопах был прав.

Offline

Какая основная "фишка" прогрессивных web-приложений?

Способность работать в режиме offline.

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

Ввод-вывод

По способу ввода ("сенсорный экран" против "клавиатура + мышь") и отображения информации (размер дисплея в дюймах) устройства можно разделить на две больших группы:

  • смартфоны и планшеты

  • ноутбуки и десктопы

Web-интерфейсы

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

IndexedDB

Для "осознанного" кэширования запросов (например, на уровне service worker'ов) современные браузеры предоставляют Cache API, но основным хранилищем данных при работе в режиме offline является IndexedDB. Серверная база данных общего пользования (MySQL, Postgres, Oracle, MongoDB, ...) в этот момент заменяется базой данных персонального пользования (IndexedDB).

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

SEO

В моей статье "Влияние service worker'ов на web-приложения" я пришёл вот к такой схеме работы PWA:

При общении браузера с сервером возникают два потока информации:

  • статика: это код (HTML/CSS/JS) и медиа-файлы (в основном изображения), которые могут кэшироваться service worker'ом;

  • API: это пользовательские данные, которые так или иначе должны мигрировать между серверным хранилищем (DB) и персональным хранилищем (IndexedDB);

PWA - это прежде всего альтернатива native apps для мобильных устройств. Native apps также можно рассматривать с позиции, что статика (код + медиа) сворачивается в один пакет и распространяется через App Store или Google Play, а данные передаются между приложением и сервером через API (с учётом offline/online состояния подключения). И при этом никто не ожидает, что поисковики должны индексировать API-запросы native apps. От web-приложений же наоборот ждут, что любой адрес (страница) на сервере должен иметь возможность быть собранным на стороне сервера, в том числе и для индексации поисковиками.

Резюме

PWA - это, в первую очередь, альтернатива native apps для мобильных устройств.

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

Особенности взаимодействия пользователя со смартфоном/планшетом (ввод-вывод, гео-позиционирование) достаточно сильно отличаются от особенностей взаимодействия пользователя с десктопом/ноутбуком. Создание универсального решения ("амфибии") добавляет сложности по отношению к двум решениям - отдельно для смартфонов/планшетов и отдельно для ноутбуков/десктопов. Причём не столько с точки зрения кодирования, сколько с точки зрения UX.

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

Подробнее..

Знакомство с App Gallery. Создаем аккаунт разработчика

22.09.2020 18:18:39 | Автор: admin


Что происходит, кто виноват и что делать


Недавно Google прекратил сотрудничество с Huawei. Это привело к тому, что Huawei на своих новых девайсах уже не может использовать сервисы Google (магазин приложений, геолокация, карты, пуши, аналитика etc), что для пользователя превращает девайс в кирпич. Если бы это не была китайская компания, то, скорее всего, на этом её бизнес, связанный с Android, просто бы прекратился. Но компания китайская, большая и они пошли по пути импортозамещения, в кратчайшие сроки реализовав функционал, аналогичный Google сервисам.


В этой серии статей мы хотим поделиться своим опытом использования Huawei Mobile Services в уже готовом приложении, использующем Google Mobile Services для аналитики (Firebase Analytics), карт и геолокации. Текста получилось довольно много и о сильно разных сервисах, засим статей будет несколько. Начнём мы с основ регистрации аккаунта разработчика и базовых вещей в коде.


  1. Создаём аккаунт разработчика, подключаем зависимости, подготавливаем код к внедрению. вы тут
  2. Встраиваем Huawei Analytics.
  3. Используем геолокацию от Huawei.
  4. Huawei maps. Используем вместо Google maps для AppGallery.

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


Что нужно для успешного внедрения


Всё было бы просто, если бы приложение писалось с нуля и не нужно было бы поддерживать как Google так и Huawei. Но мы живём в реальном мире и без сложностей не обойтись. Однако дело сильно упростится, если соблюдён ряд условий.


Но перед перечислением условий надо составить ТЗ. Оно у нас получилось такое:


  1. Нам нужно получить 2 версии APK одну для Google Play, с библиотеками от Google, другую для AppGallery, с библиотеками от Huawei.
  2. В приложении уже используется Firebase Analytics. Надо его заменить на аналог от Huawei.
  3. Есть определение местоположения пользователя. Аналогично заменяем на аналог.
  4. Есть карты. Нужно также заменить на аналог, по максимуму сохранив функционал, т.к. в реализации от Huawei некоторые вещи ещё не сделаны.

А вот что сильно сократит прикладываемые усилия:


  1. Код должен быть написан хорошо. И быть без багов (хотя это само собой разумеется зачем код с багами писать?). Под хорошо будем подразумевать более-менее стандартную архитектуру, мимикрирующую под Clean.
  2. Если код из Google библиотек размазан ровным слоем по всему проекту, то у меня для вас плохие новости. Например у вас может не быть абстракции над аналитикой и/или над полученными от Google координатами. В этом случае придётся её завести, чтобы почистить код от импортов гугловых классов, которые будут недоступны, когда мы уберём их из сборки.
  3. Использование DI. Очень упрощает абстрагирование над аналитикой и геолокацией. Используем интерфейсы, через DI передавая нужную реализацию.
  4. Карты не слишком сильно кастомизированы. В частности, основная сложность будет с абстрагированием над кластеризацией маркеров.

Подготовка к внедрению


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


  1. Регистрируемся на https://developer.huawei.com. Тут понадобится паспорт/права + пластиковая карта. День-два вас будут проверять, потом аккаунт заработает. Если вдруг что-то пойдёт не так (забудете что-то указать или укажете неправильно) вам напишут и подробно объяснят. После общения с Google Play всё выглядит очень круто русскоязычная техподдержка отвечает быстро и по делу.
  2. Принимаем всякие соглашения об обработке персональных данных. Внимательно читая, конечно же)
  3. Создаём проект приложения, указывая пакет (он же ApplicationId).
  4. Если вам нужно ещё и встроенные покупки реализовать то надо: а) Заполнить данные банковского счёта б) Распечатать и заполнить заявление о трансграничной передаче персональных данных в КНР в) Отправить скан оного вместе с данными из пункта а г) Отправить заявление из пункта б по почте в Москву. Когда заявление дойдёт вам придёт e-mail и останется только активировать сервис в настройках проекта. На почте бывают накладки возможно, придётся подождать. Я пару недель ждал, потом позвонил ответственному за это в Huawei уверили, что проблему решат. И решили. На русском тоже всё общение очень круто)
  5. Включаем сервис аналитики. В отличие от геолокации и карт, включённых по умолчанию, это нужно сделать вручную.
  6. Добавляем SHA-256 для всех ключей, которыми будет подписано приложение. Т.е. дебажные ключи и релизный ключ.
  7. Скачиваем аналог google-services.json, в случае Huawei называемый agconnect-services.json
  8. Создаём разные flavors для Google и Huawei. Наконец-то можно перейти к коду:

В build.gradle (module app) создаём flavors и указываем, что в папках src/google/kotlin, src/google/res, src/huawei/kotlin, src/huawei/res также находиться будет наш код.


android {  ...  sourceSets {      google.java.srcDirs += 'src/google/kotlin'      google.res.srcDirs += 'src/google/res'      huawei.java.srcDirs += 'src/huawei/kotlin'      huawei.res.srcDirs += 'src/huawei/res'  }  flavorDimensions "store"  productFlavors {      google {          dimension "store"      }      huawei {          dimension "store"      }  }}

Также создаём папки src/huaweiDebug и src/huaweiRelease. В них помещаем наш файл конфигурации agconnect-services.json


И добавляем apply plugin: 'com.huawei.agconnect' в конец build.gradle (module app).


И наконец, добавляем в build.gradle проекта:


buildscript {    ...    repositories {        ...        maven {url 'https://developer.huawei.com/repo/'}    }    dependencies {        ...        classpath 'com.huawei.agconnect:agcp:1.2.1.301'    }}allprojects {    repositories {        ...        maven {url 'https://developer.huawei.com/repo/'}    }}

В следующей части встраиваем аналитику


Теперь мы полностью готовы. У нас есть 2 разных варианта сборки для Huawei и Google. У нас подключены необходимые зависимости. Созданы папки, где будет наш код. Создан аккаунт разработчика и выполнены необходимые действия по созданию проекта приложения. У нас даже какое-то ТЗ есть. И мы уже выполнили первый пункт из ТЗ! Отличный повод на этом статью закончить. И уже в следующей встроить аналитику не от Google, а от Huawei.


Весь код, который есть в этом цикле статей вы можете посмотреть в репозитории на GitHub. Вот ссылка.

Подробнее..

Как захватить новую страну за 3 недели

11.09.2020 12:04:11 | Автор: admin
Представим сферическую сеть пиццерий в вакууме, которая хочет захватить мир (никогда такого не было и вот опять). Она уже открыла пиццерии в 13 странах мира и планирует увеличивать эту цифру. Всего год назад запуск (сайта, приложения и информационной системы) был редким 1 страна за год, а сейчас срок сократился до 3 недель. Что мешало сделать это раньше и как получилось ускориться, расскажем в статье.



Dodo Pizza международная компания. Мы работаем в 13 странах и не планируем останавливаться. Большая часть пиццерий расположена в регионе Евразия: Россия, Казахстан, Беларусь, Кыргызстан, Узбекистан. Это уже большой действующий бизнес, мы лидеры по количеству пиццерий. Этот бизнес надо только поддерживать и развивать вглубь, потому что здесь бизнес и IT работают вместе над понятными фичами.

  • Основная часть разработчиков сосредоточена на развитии бизнеса в Евразии.
  • Разработчики работают в командах.
  • Команды разделены по продуктам.
  • Внутри каждого продукта несколько команд.
  • Каждый продукт прокачивает свои метрики: ресторан ускоряет обслуживание клиентов в зале, доставка увеличивает LTV и средний чек в приложении и на сайте.

И тут (внезапно) приходит бизнес и говорит: Хотим запускаться в Нигерии (та самая 13-я страна, в которой работают уже 2 пиццерии) До 2019 года запуск был редким 1 страна в год. Команда разработки, которая сопровождала запуск, постоянно менялась. Фокуса на ускорении при таком подходе не было.

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

Трудности запуска для бизнеса


Открытие новой страны сюрприз (может быть). Иногда в предпринимательстве появляются возможности, которые просто нельзя упускать, даже если не готов. Например:

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

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

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

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

Расфокус. Из-за единичных запусков команде разработки, которая будет заниматься новой страной придётся отрываться от киллер-фич для Евразии ради сомнительной выгоды в Европе или Африке. Новая страна, первая пиццерия, инвестиции окупятся через 1-2 года многое может пойти не так.

Много компонентов, огромная система и много связей нет выстроенной системы запуска и экспертов. Запуск новой страны, с точки зрения Dodo IS, это не просто копипаст кода или нажатие одной волшебной кнопки. Это отдельный проект со своими локальными особенностями. Это 3-4 месяца на запуск (иногда и дольше).

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

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

Технические трудности


Dodo Pizza появилась в апреле 2011 года. В июне 2011 началась разработка Dodo IS. В то время никто даже не думал, что скоро мы будем запускать пиццерии в других странах, потому что нужно было быстро (очень-очень быстро) поддерживать растущий бизнес в России. Например, первая касса ресторана появилась в системе за 2 недели разработки, так как без неё нельзя было открывать ресторан в первой пиццерии.

Не было времени всё хорошенько продумать и заложить масштабируемость и country-agnostic компоненты в архитектуру системы. Меньше всего мы думали, что код, который сейчас пишем, будет использоваться где-нибудь в Словении или США. Поэтому за годы бурного роста накопилось много технического долга, который сейчас замедляет запуск.

Развернуть сайт и бэк-офис Dodo IS это долго. Нельзя просто так взять и прописать в конфигах Nginx домен для новой страны и развернуть систему по кнопке. А жаль.

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

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

В Excel мы выгружали партнёру строки с терминами, он переводил (без контекста и картинок), мы загружали Excel с переводами обратно и повторяли упражнение. Это не то, для чего создан Excel. Да, и в ресурсах, к сожалению, были не все проекты и строки код приходилось править.


Пример файла с переводами.

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

Кассы и налоги. Касса ресторана и касса доставки это компоненты Dodo IS. Без них ничего нельзя продать ни в ресторане, ни на доставку. Кассы необходимо адаптировать для новой страны, а код в монолите и он тянет за собой множество зависимостей. Получается, кроме бизнес-проработки (налоги, ставки, требования к чеку), необходимо аккуратно написать логику для новой страны, так, чтобы не сломалась печать чеков в России.

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

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

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

Как решили бизнес-проблемы


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

Мы выделили новый независимый продукт запуск новых стран и поставили амбициозную цель запуск страны по кнопке. С кнопкой мы могли бы:

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

Команда. Цель невозможно достичь без команды. Поэтому в мае 2019 года команда MyLittleCoders согласилась стать командой открытия стран на 100% времени. Мы выделили открытие в новый продукт: метрика скорость запуска есть, команда есть, бэклог по ускорению полон задачами через край. Всё сошлось пора действовать.


Логотип команды MyLittleCoders (MLC)

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

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

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


Фрагмент story map запуска новой страны.

Как решили технические проблемы


Мастер начальной настройки страны. Анализ story map показал, что часть этапов можно автоматизировать, что значительно ускорит запуск. Поэтому первым существенным улучшением стал мастер начальной настройки страны или Country Wizard.

После покупки нового iPhone настройки со старого телефона переносятся за пару кликов 3 экрана и новый телефон готов к работе. Мы хотели сделать для Dodo IS что-то похожее.

  • Запускаешь мастер начальной настройки страны.
  • Говоришь: Запускаю Нигерию.
  • Выбираешь язык, валюту, вводишь маску телефона, типы улиц и типы населенных пунктов.
  • Вводишь логин и пароль для временного пользователя (для первого входа в систему).
  • Next, next, next и готово заходишь в свежеразвёртную копию системы для Нигерии и создаёшь маркетологам и бизнес-девелоперам их персональные учётки.

В ходе работы концепция поменялась. Изначально мы хотели добавить все-все настройки системы в Country wizard (РО продукта хотел и грезил, команда была сдержанно-оптимистична). Но у нас уже была внутренняя админка, дублировать которую в мастере настройки оказалось бессмысленно. Тогда мы оставили в нём только тот минимум настроек, без которых система просто не могла запуститься. Продукты, меню, цены поставщиков и тару можно донастроить уже потом.

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

В итоге, всё получилось прекрасно. Вместо множества мест, где надо что-то править мы:

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

Два дня (а не месяц как раньше) и система готова к работе.


Пример настройки параметров доставки через мастер начальной настройки.

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

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


Фрагмент чек-листа запуска.

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

Настройки страны из одной точки. Собрать все настройки из Country wizard недостаточно. Важно, чтобы система и её компоненты также читали эти настройки из одного хранилища. Иначе получаются забавные ситуации. Например, когда менеджер офиса показывает правильную валюту (для Нигерии найра), а витрина кусочков предательски показывает рубли. Каждый сервис считал своим долгом завести собственные настройки. Приходилось проходить 7-8 мест в системе, чтобы все наконец показывали правильную валюту.

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

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

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

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

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

Мы поискали сервисы для локализации, пообщались с поддержкой и остановились на сервисе Crowdin. Простой, с разграничением ролей подошёл как влитой. Особенно понравилась фича in-context. Когда открываем сайт или бэк-офис на специальном окружении и правим строки прямо в интерфейсе. Так сразу видно где и что конкретно исправляем удобно.


Crowdin in-context редактирование.

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

Больше никакого Excel.

Вся локализация упростилась: партнёр теперь может делать переводы заранее и без помощи разработчиков. Так что когда мы готовы разворачивать Dodo IS и сайт, переводы обычно готовы.

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

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

Что в итоге


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

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

Планы


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

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

Приложение. С января 2020 года к нам присоединилась мобильная команда Легионеры (3 человека). Теперь нам надо раздать долги запустить приложение во всех странах, и научиться запускать новые страны сразу с приложением.

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

Напоследок


Запустить страну это полдела теперь нужно ещё и поддерживать существующих партнёров (которых только что стало +1):

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

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

Именно поэтому мы собираем новую команду под регион ЕМЕА. Эта команда будет адаптировать систему под локальные рынки, создавая ту самую уникальность, отличающую бизнес в UK от бизнеса в Нигерии. Мы ищем в команду опытных разработчиков. Если интересно открывать мир, запускать новые пиццерии на карте и решать не рутинные задачи ждем вас в команду. Напишите мне на d.pavlov@dodopizza.com буду рад пообщаться:)

Примечание. В посте Катя Ландырева подробнее описала, что вас ждет чуть и почему круто работать в нашем продукте.
Подробнее..

Российские пасхалки в мобильных приложениях. Какие они?

11.09.2020 14:12:59 | Автор: admin
Привет, Хабр! Уже завтра День программиста (12.09.2020), и специально к нашему профессиональному празднику я написал не хардкорно-технический пост, а лайтовую статью о маленьких, не всегда очевидных фичах, которые хоть и не часто, но встречаются в мобильных приложениях и не только. Как вы уже догадались это пасхалки. И не просто пасхалки, а отечественного производства. Если хотите ненадолго погрузиться в детали, которые мы обычно не замечаем, либо которые сложно найти, добро пожаловать под кат.



Для тех, кто не знаком с понятием пасхальное яйцо небольшая выдержка из википедии:
Пасхальное яйцо (англ. Easter Egg) секрет в компьютерной игре, фильме или программном обеспечении, заложенный создателями. Отличие пасхального яйца в игре от обычного игрового секрета состоит в том, что его содержание, как правило, не вписывается в общую концепцию, выглядит в контексте неправдоподобно, нелепо, и зачастую является внешней ссылкой. Пасхальные яйца играют роль своеобразных шуток для внимательных игроков или пользователей, но могут применяться в целях защиты авторских прав.
Чаще всего для получения пасхального яйца следует произвести сложную или нестандартную совокупность действий, что делает маловероятным случайное обнаружение. Название происходит от популярного в США и бывших Британских колониях семейного мероприятия охота за яйцами (англ. egg hunt), устраиваемого накануне Пасхи, в котором участники должны с помощью подсказок найти как можно больше спрятанных по местности яиц.
Мы опросили наших коллег по цеху из отечественных компаний о наличии пасхалок в мобильных приложениях, и вот какой обзор в результате получился:

Lamoda и OneTwoTrip


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

Joom


В Android-приложении есть змейка. Инструкция для активации: профиль -> настройки -> About -> Провести пальцем по логотипу от верхней части до точки. Обязательно играйте со звуком!


Tinkoff


При выборе диапазона дат в анализе трат по карте 3-го сентября календарь переворачивается.


Сберавто


Если в поиске находится слишком много предложений (число не уточняется), то вместо количества будет написано видимо-невидимо или рука листать устанет. А также в честь дня программиста в приложении появилась игра 2048. Если в поиске ввести число 256 или 2048 она активируется.

VK


В iOS приложении анимация обновления иногда (в случайном порядке) меняется на котика.


Ангстрем


Этот конвертер величин есть только на iOS. При листании экранов вправо последним экраном будет смайлик, и если его вытащить достаточно далеко, он подмигнёт.


2GIS


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

iFunny


И, конечно же, флагман нашей компании.
В приложении есть аккаунт c игровым контентом в виде Web Apps, под названием iFunnyArcade_2017. Про их разработку мы писали тут.


Android OS


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

Для пользователей:
Нужно зайти в настройки -> о системе -> версия OS -> 3 раза быстро тапнуть по версии OS (количество может отличаться от версии системы).
Android 10 Android 11

Для разработчиков:
В коде класса Chronometer есть метод isTheFinalCountDown, при вызове которого произойдёт переход по ссылке www.youtube.com/watch?v=9jK-NcRmVcw (клип на песню группы Europe The Final Countdown).
    /**     * @return whether this is the final countdown     */    public boolean isTheFinalCountDown() {        try {            getContext().startActivity(                    new Intent(Intent.ACTION_VIEW, Uri.parse("https://youtu.be/9jK-NcRmVcw"))                            .addCategory(Intent.CATEGORY_BROWSABLE)                            .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT                                    | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT));            return true;        } catch (Exception e) {            return false;        }    }

Также у класса SensorManager есть константа-отсылка к Звёздным войнам.
/** Gravity (estimate) on the first Death Star in Empire units (m/s^2) */public static final float GRAVITY_DEATH_STAR_I = 0.000000353036145f;


А где ещё?


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

Перевод 20 инструментов Android-разработчика, о которых вы могли не знать

14.09.2020 18:23:30 | Автор: admin

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

Работая над статьями о 30 лучших библиотеках и проектах Android 2019 г. и 25 лучших библиотеках и проектах Android 2020 г., я наткнулся на множество замечательных инструментов и проектов, которые могут пригодиться в разработке приложений для Android ниже они приведены в случайном порядке. Пользуйтесь.

1. AinD Android (Anbox) в Докере

AinD запускает приложения Android, помещая контейнеры Anbox в Докер.

В отличие от аналогичных проектов на основе виртуальных машин, AinD может выполняться на экземплярах IaaS без поддержки вложенной виртуализации. Docker Hub: aind/aind.

Предназначение:

2. Booster

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

Booster это набор модулей для оценки производительности, оптимизации многопоточности, встроенного индекса ресурсов, сокращения числа избыточных ресурсов, сжатия ресурсов, исправления системных ошибок и т. д. Booster позволяет повысить стабильность приложения на 1525% и снизить размер пакета на 110 МБ.

Документация очень хорошая, лицензия Apache 2.0.

3. Shake

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

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

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

4. Scabbard

Scabbard помогает с визуализацией и анализом графика зависимостей Dagger 2.

Scabbard визуализирует точки входа, схемы зависимостей, взаимосвязи компонентов и области действия. Добавить этот инструмент в проект очень легко: он хорошо интегрирован с Gradle, а также с Android Studio и IntelliJ (нажав значок на левом поле в редакторе, можно просмотреть схему для @Component или @Subcomponent).

Документирован проект отлично: есть множество примеров и подсказок.

Лицензия Apache 2.0.

5. Can I Drop Jetifier?

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

Всё больше и больше библиотек переходят на AndroidX, поэтому в какой-то момент необходимость включать этот инструмент отпадает. Этот плагин определяет, какие из используемых библиотек нужно перенести на AndroidX или избавиться от них, если уже вышла новая версия, Can I Drop Jetifier?

Документация понятная, проект выпущен под лицензией Apache 2.0. Очень рекомендую!

6. ADB Event Mirror

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

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

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

7. Android Emulator Container Scripts

Android Emulator Container Scripts набор небольших сценариев для запуска эмулятора в контейнере для различных систем (например, для Докера) с целью внешнего использования. Сценарии совместимы с Python версий 2 и 3. Этот репозиторий довольно популярен и пригодится, если нужно запускать много эмуляторов на удаленных машинах.

Проект выпущен под лицензией Apache 2.0 и хорошо документирован.

8. Autoplay

Autoplay это плагин для Gradle, предназначенный для публикации артефактов Android в Google Play.

Его можно считать очень простой альтернативой Gradle Play Publisher или Fastlane. Опубликовать приложение можно как apk или набор App Bundle.

Особенности Autoplay:

  • Оптимизирован для использования в CI/CD.

  • Удобен для разработчиков.

  • Надежен и перспективен.

У проекта хорошая документация, версия на момент написания статьи 1.3.0, лицензия Apache 2.0.

9. Плагин Gradle для статического анализа

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

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

10. AndroidUtilCode

AndroidUtilCode функциональная и простая в использовании библиотека для Android, которая инкапсулирует функции, обычно используемые при разработке Android с демонстрационными версиями и модульными тестами. Инкапсулированные API позволяют значительно повысить эффективность разработки.

Проект состоит в основном из двух модулей: utilcode (используется в разработке часто) и subutil (используется редко, но позволяет упростить основной модуль).

Версия проекта 1.29.0, лицензия Apache 2.0.

11. Hijckr

Hijckr вмешивается в инфляцию макета Android и перенаправляет названные элементы в другие классы.

Это довольно интересный инструмент. Например, если файл макета содержит TextView, Android обычно загружает android.widget.TextView, но вместо этого можно перехватить xml-теги и загрузить com.myapp.TextView.

Описание проекта довольно подробное и позволяет быстро начать работу с инструментом (который полностью написан на Java).

12. Roomigrant

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

Проект выпущен под лицензией MIT, версия 0.1.7.

13. RoomExplorer

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

Инструмент хорошо документирован, лицензия Apache 2.0.

14. Android Framer

Инструмент android-framer добавляет рамки и заголовки к скриншотам в Google Play. Источник вдохновения fastlane frameit.

Инструмент написан на Python и использует ImageMagick. Настроить рамки (фоны) можно, например, с помощью Facebook Design. Также можно менять шрифт, кегль, размер рамки и т. д.

Лицензия Apache 2.0.

15. Dependency Tree Diff

Dependency Tree Diff это интеллектуальный инструмент сравнения для вывода задачи dependencies Gradle, который всегда показывает путь к корневой зависимости.

Можно установить инструмент через brew или просто использовать jar-файл.

Лицензия Apache 2.0.

16. Gradle Doctor

Gradle Doctor это плагин для сканирования сборки Gradle. Функциональность: настраиваемые предупреждения о проблемах со скоростью сборки, измерение временных затрат на инструменты обработки аннотаций Dagger, установка переменной JAVA_HOME и проверка ее соответствия JAVA_HOME в IDE, простое отключение кеширования тестов, остановка сборки в случае, если найдены пустые каталоги src (поскольку это может быть причиной несовпадений в кеше), и многое другое.

У инструмента отличная документация, проект выпущен под лицензией Apache 2.0.

17. GloballyDynamic

GloballyDynamic это набор инструментов, направленных на обеспечение всеобщей доступности Dynamic Delivery, независимо от магазина приложений или платформы распространения, которые также предоставляют единый унифицированный клиентский API для Android и простой интерфейс для разработчиков.

Поддерживаются:

Рекомендую прочитать README и подробнее ознакомиться с этим инструментом.

Лицензия Apache 2.0.

18. Dagger Browser

Dagger Browser еще один инструмент (прогрессивное веб-приложение) для удобной навигации по схеме Dagger в проекте.

Данные схемы заполняются с помощью SPI-плагина Dagger, а средство просмотра написано с помощью CRA (create-react-app) и TypeScript, Dagger Browser

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

19. Wormhole

Wormhole путешествующий во времени инструмент преобразования байт-кода, добавляющий в android.jar будущие API-интерфейсы, которые можно десахаризовать на все уровни API с помощью D8 и R8.

Wormhole обеспечивает обратную совместимость с более новыми API. Приведу пример.

В Android R есть новые методы из Java 9 например, List.of. Благодаря D8 и R8 они не являются эксклюзивными для API 30 и мгновенно превращаются в совместимые с API 1. В D8 и R8 есть набор методов десахаризации для API, которых еще нет в android.jar. И можно не ждать, пока они появятся этот проект дает возможность использовать их сразу же.

20. MNML

MNML (произносится как minimal минимальный) простое бесплатное приложение для записи экрана в Android.

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

Лицензия Apache 2.0.

Заключение

Вот и всё. Надеюсь, список вам понравился и какие-то инструменты смогли вас вдохновить. До встречи!

О переводчике

Перевод статьи выполнен в Alconost.

Alconost занимается локализацией игр, приложений и сайтов на 70 языков. Переводчики-носители языка, лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджеры проектов 24/7, любые форматы строковых ресурсов.

Мы также делаем рекламные и обучающие видеоролики для сайтов, продающие, имиджевые, рекламные, обучающие, тизеры, эксплейнеры, трейлеры для Google Play и App Store.

Подробнее..

Формальные грамматики на службе мобильного клиента

17.09.2020 12:16:31 | Автор: admin
В повседневной жизни мы пользуемся готовыми интерпретаторами и компиляторами и редко кому придёт в голову написать их самостоятельно. Во-первых, это же сложно, во-вторых зачем.

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

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



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

В конце концов, заполненную форму мобильное приложение отправляет на сервер. В этот момент важно, чтобы данные были правильно отформатированы. Например, API интернет-банка будет требовать, чтобы номер выглядел вот так: 9161234567 без 8, скобочек и минусов.

Если форма пассивно обрабатывает пользовательские данные, то номер телефона она принимает в любом формате, но возникают проблемы:

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



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

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

Если продолжать пример с номером телефона, использование маски даст следующие преимущества:

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



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


Почему нельзя просто взять и описать маску


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

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

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

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

  1. понять правила построения исходной грамматики,
  2. понять правила построения целевой грамматики,
  3. написать правила перевода из исходной грамматики в целевую,
  4. реализовать всё это в коде.

Это то, для чего и пишутся компиляторы и трансляторы.

Теперь подробно рассмотрим наше решение на основе формальных грамматик.

Предыстория


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


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



Давайте посмотрим, как устроены маски.

Примеры масок в разных форматах


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



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

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



Дальше идёт динамическая часть она всегда выделена угловыми скобками:


Далее в тексте я буду называть это выражение динамическим выражением или ДВ сокращённо


Здесь записано выражение, по которому мы будем форматировать наш ввод:



Красным выделены кусочки, отвечающие за содержимое динамической части.

\\d любая цифра.

+ обычный репитер: повторить минимум один раз.

${3} символ метаинформации, который уточняет количество повторений. В данном случае должно быть три символа.

Тогда выражение \\d+${3} означает, что должно быть три цифры.

В данном формате масок внутри динамической части может быть только один репитер:



Это ограничение появилось не просто так сейчас объясню почему.
Допустим, у нас есть ДВ, в котором жёстко указан размер: 4 элемента. И мы задаём ему 2 элемента с репитером: `<!^\\d+\\v+${4}>`. Под такое ДВ попадают следующие сочетания:

  • 1abc
  • 12ab
  • 123a

Получается, что такое ДВ не даёт нам однозначного ответа, чего ожидать на месте второго символа: цифру или букву.

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



На клиенте формат у масок может выглядеть по-другому. Например, в библиотеке Input Mask от Redmadrobot маска для номера телефона имеет следующий вид:



Выглядит она симпатичнее и понимать её проще.

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



Переформулируем задачу: как совместить маски разных форматов


Нам нужно эти маски друг с другом совместить или как-то из одной получить вторую.



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

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

Так как мы дошли до интерпретатора, давайте поговорим про грамматики.

Как проводится синтаксический анализ




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

Этот процесс называется токенизация: поток символов превращается в поток токенов. Количество токенов ограничено, они формализованы, следовательно, можно их анализировать.

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

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



Все константы представим как токен CS, у которого аргумент сама константа:


Следующий вид токенов это начало ДВ:


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



Затем у нас идёт репитер.



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



Конец ДВ. Таким образом, мы разложили всё по токенам.



Пример токенизации маски для номера телефона


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



Сначала символ +. Преобразуем в константный символ +. Далее то же самое мы делаем для семёрки и для всех остальных символов. Получаем массив из токенов. Это ещё не структура далее будем этот массив анализировать.

Лексер и построение АСД


Теперь более сложная часть это лексер.



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

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

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

Дальше всё похожим образом выглядит. Если это ДВ, то это либо symbol, либо repeater. В нашем случае это правило шире. И в конце обязательно должен быть токен с метаданными.
Последнее правило maskRule. Это последовательность из символов и ДВ.

Теперь построим абстрактное синтаксическое дерево (АСД) из массива токенов.

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



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



То же самое делаем со всеми остальными константными символами, но дальше сложнее. Мы наткнулись на токен ДВ.



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



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



На самом деле, в данном случае, нет. У нас дочерним узлом будет репитер.



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

Логика зависит от типа узла или от типа токена, который лежит в узле. Для разбора сильно удобнее сразу понимать, какой токен перед тобой: составной, как репитер, или простой, как CS. Это нужно, чтобы не возникало двойных трактовок или постоянных поисков дочерних узлов.

Особенно это было бы заметно на группах символов: например, [abcde]. В том случае, очевидно, должен быть какой-то родительский узел GROUP, у которого будет список дочерних узлов CS(a)CS(b) и т.д.

Возвращаемся к токену с метаданными. Он не включается в контент, он находится сбоку.



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

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



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

Дальше просто обычные константные символы.



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

Синтаксис библиотеки InputMask от Redmadrobot


Рассмотрим синтаксис библиотеки Redmadrobot.



Здесь то же самое выражение. +7 константа, которая добавится автоматически. Внутри фигурных скобок описано ДВ динамическая часть. Внутри ДВ специальный символ d. У Redmadrobot это дефолтная нотация, которая обозначает цифру.

Так выглядит нотация:



Нотация состоит из трёх частей:

  • character символ, который мы будем использовать, чтобы записать маску. То, из чего состоит алфавит маски. Например, d.
  • characterSet какие набранные пользователем символы матчатся этой нотацией. Например, 0, 1, 2, 3, 4 и так далее.
  • isOptional обязательно ли пользователь должен ввести один из символов characterSet или можно ничего не вводить.

Смотрим, у нас сейчас будет такая маска.



  • У символа b специальная нотация цифры и он не опциональный.
  • У символа c другая нотация CharacterSet другой. Он тоже не опциональный.
  • И символ C это то же самое что c, только он опциональный. Это нужно для того, чтобы в маске мы посмотрели на метаданные и увидели, что там не жёсткое ограничение, а слабое.

Если нужно записать правило, когда символов может быть от одного до десяти, то один символ будет не опциональный. А девять символов будут опциональными. То есть в нотации из примера они будут записаны большими буквами. В итоге это правило будет выглядеть так: [cCCCCCCCCC]

Пример: перевод маски номера телефона из формата бекэнда в формат InputMask


Вот дерево, которое мы получили на прошлом этапе. Нам нужно по нему пройтись. Первое, куда мы попадаем, это корень.



Дальше от корня мы попадаем в константный символ + генерируем сразу +. Справа записывается маска в формате InputMask.



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

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



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



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



Доходим, наконец, до какого-то контентного символа.



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

Вот мы его написали, возвращаемся и идём как раз за метаинформацией.



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



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

На практике мы взяли одну грамматику и сгенерировали из неё другую грамматику.

Правила генерации клиентской грамматики из серверной


Теперь немного про правила генерации. Это важно.

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



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

Дальше у нас идёт символ \\d.

Дальше ДВ с опциональным размером.



Первый, получается, какой-то символ b. У него будет Character Set, содержащий abcd.
Далее понятно, что будет другой уже символ, потому что не сматчишь иначе, или сматчишь неправильно. И дальше у нас это выражение превращается вот в такое.

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

Соберём всё вместе.



Здесь приведён пример Character Set, которые генерируются. Видно, что b соответствует Character Set abcd, для цифр соответствующий предустановленный Character Set. Для d и D соответствующий Character Set содержит 12vf.

Итоги


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

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



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

Работающая библиотека по переводу масок


Можете посмотреть на то, как мы реализовали вышеописанный подход. Библиотека лежит на Гитхабе.

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


Это первая маска, которую мы смотрели в самом начале. Она интерпретируется в такое RedMadRobot представление.



А это вторая маска просто маска ввода чего-то. Она конвертируется в такое представление.

Подробнее..

Виджеты в iOS 14 возможности и ограничения

17.09.2020 14:13:09 | Автор: admin


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

Начнем с определения виджетами называются представления, показывающие актуальную информацию без запуска основного мобильного приложения и всегда находящиеся под рукой у пользователя. Возможность их использовать уже есть в iOS (Today Extension), начиная с iOS 8, но мой сугубо личный опыт их использования довольно печален под них хоть и выделен специальный рабочий стол с виджетами, но я всё равно редко туда попадаю, привычка не выработалась.

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



Работа с картами лояльности одна из основных функций нашего приложения Кошелёк. Периодически в отзывах в App Store появляются предложения от пользователей о возможности добавления виджета в Today. Пользователи, находясь у кассы, хотели бы как можно быстрее показать карту, получить скидку и убежать по своим делам, ведь промедление на любой квант времени вызывает те самые укоризненные взгляды в очереди. В нашем случае виджет может сэкономить несколько пользовательских действий для открытия карты, таким образом сделав операцию оплаты товара на кассе более быстрой. Магазины тоже будут благодарны меньше очередей на кассе.

В этом году Apple неожиданно для всех выпустила релиз iOS практически сразу после презентации, оставив разработчикам сутки на доработки своих приложений на Xcode GM, но мы оказались готовы к релизу, так как свой вариант виджета наша iOS-команда стала делать еще на бета-версиях Xcode. Сейчас виджет находится на ревью в App Store. Обновление устройств до новой iOS по статистике происходит довольно быстро; скорее всего, пользователи пойдут проверять, у каких приложений виджеты уже есть, найдут наш и будут счастливы.

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



Добавление виджета в проект


Как и другие подобные дополнительные возможности, виджет добавляется как расширение (extension) к основному проекту. После добавления Xcode любезно генерирует код виджета и других основных классов. Вот тут нас ждала первая интересная особенность для нашего проекта этот код не компилировался, так как в одном из файлов автоматически подставлялся префикс в названиях класса (да-да, те самые Obj-C префиксы!), а в генерируемых файлах нет. Как говорится, не боги горшки обжигают, видимо, разные команды внутри Apple не договорились между собой. Будем надеяться, что к релизной версии исправят. Для того, чтобы настроить префикс своего проекта, в File Inspector основного таргета приложения заполните поле Class Prefix.

Для тех, кто следил за новинками WWDC, не секрет, что реализация виджетов возможна только с использованием SwiftUI. Интересный момент, что таким образом Apple форсит обновление на свои технологии: даже если основное приложение написано с использованием UIKit, то тут, будьте любезны, только SwiftUI. С другой стороны, это хорошая возможность попробовать новый фреймворк для написания фичи, в этом случае он удобно вписывается в процесс никаких изменений состояния, никакой навигации, требуется только задекларировать статичный UI. То есть вместе с новым фреймворком появились и новые ограничения, потому как старые виджеты в Today могут содержать больше логики и анимацию.

Одно из основных нововведений SwiftUI возможность предпросмотра без запуска на симуляторе или девайсе (preview). Классная вещь, но, к сожалению, на больших проектах (в нашем ~400K строк кода) работает крайне медленно даже на топовых макбуках, быстрее запустить на девайсе. Альтернатива такому способу иметь под рукой пустой проект или плейграунд для быстрого прототипирования.

Возможность для дебага также есть с помощью выделенной Xcode схемы. На симуляторе отладка работает нестабильно даже к версии Xcode 12 beta 6, поэтому лучше пожертвовать один из тестовых девайсов, обновить до iOS 14 и тестировать на нем. Будьте готовы, что эта часть и на релизных версиях будет работать не как ожидается.

Интерфейс


На выбор пользователю даются разные типы (WidgetFamily) виджетов трёх размеров small, medium, large.



Для регистрации необходимо явно указать поддерживаемые:
struct CardListWidget: Widget {    public var body: some WidgetConfiguration {        IntentConfiguration(kind: CardListWidgetKind,                            intent: DynamicMultiSelectionIntent.self,                            provider: CardListProvider()) { entry in            CardListEntryView(entry: entry)        }        .configurationDisplayName("Быстрый доступ")        .description("Карты, которые вы используете чаще всего")        .supportedFamilies([.systemSmall, .systemMedium])    }}

Мы с командой решили остановиться на small и medium выводить одну любимую карту для маленького виджета или 4 для medium.

Добавление виджета на рабочий стол происходит из центра управления, там пользователь выбирает необходимый ему вид:



Цвет кнопки Добавить виджет кастомизируем с помощью Assets.xcassets -> AccentColor, имя виджета с описанием тоже (пример кода выше).

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

@mainstruct WalletBundle: WidgetBundle {    @WidgetBundleBuilder    var body: some Widget {        CardListWidget()        MySecondWidget()    }}

Так как виджет показывает слепок некоторого состояния, то единственная возможность для пользовательского интерактива это переход в основное приложение по нажатию на какой-то элемент или весь виджет. Никакой анимации, навигации и переходов на другие view. Но есть возможность прокинуть диплинку в основное приложение. При этом для small виджета зоной нажатия является вся область, и в этом случае используем widgetURL(_:) метод. Для medium и big доступны нажатия по view, и в этом нам поможет структура Link из SwiftUI.

Link(destination: card.url) {  CardView(card: card)}

Финальный вид виджета двух размеров получился следующим:



При проектировании интерфейса виджета могут помочь следующие правила и требования (согласно гайдлайнам Apple):
  1. Сфокусируйте виджет на одной идее и проблеме, не пытайтесь повторить всю функциональность приложения.
  2. В зависимости от размера выводите больше информации, а не просто масштабируйте контент.
  3. Выводите динамическую информацию, которая может меняться в течение дня. Крайности в виде полностью статической информации и информации, меняющейся ежеминутно, не приветствуются.
  4. Виджет должен давать актуальную информацию пользователям, а не быть еще одним способом открыть приложение.

Внешний вид настроили. Следующий шаг выбрать, какие карты и каким образом показывать пользователю. Карт может быть явно больше четырёх. Рассмотрим несколько вариантов:
  1. Дать возможность пользователю выбирать карты. Кто, как не он, знает, какие карты важнее!
  2. Показывать последние использованные карты.
  3. Сделать более умный алгоритм, ориентируясь, например, на время и день недели и статистику (если пользователь по будням вечером ходит во фруктовую лавку у дома, а на выходных ездит в гипермаркет, то можно помочь пользователю в этом моменте и показывать нужную карту)

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

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


Настройки формируются с помощью интентов (привет Андроид-разработчикам) при создании нового виджета файл интента добавляется в проект автоматически. Кодогенератор подготовит класс-наследник от INIntent, который является частью фреймворка SiriKit. В параметрах интента стоит магическая опция Intent is eligible for widgets. Доступны несколько типов параметров, можно настраивать свои подтипы. Так как данные в нашем случае это динамический список, то еще устанавливаем пункт Options are provided dynamically.

Для разных типов виджета настраиваем максимальное количество элементов в списке для small 1, для medium 4.
Этот тип интента используется виджетом как источник данных.



Далее настроенный класс интента необходимо поставить в конфигурацию IntentConfiguration.
struct CardListWidget: Widget {    public var body: some WidgetConfiguration {        IntentConfiguration(kind: WidgetConstants.widgetKind,                            intent: DynamicMultiSelectionIntent.self,                            provider: CardListProvider()) { entry in            CardListEntryView(entry: entry)        }        .configurationDisplayName("Быстрый доступ")        .description("Карты, которые вы используете чаще всего.")        .supportedFamilies([.systemSmall, .systemMedium])    }}

В случае, если пользовательские настройки не требуются, то есть альтернатива в виде класса StaticConfiguration, которая работает без указания интента.

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

iPhone 11 Pro Max28 для настроек21 для меню добавленияiPhone 11 Pro25 для настроек19 для меню добавленияiPhone SE24 для настроек19 для меню добавления

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



Также можно изменить цвет фона и значения параметров WidgetBackground и AccentColor по умолчанию они уже лежат в Assets. При необходимости их можно переименовать в конфигурации виджета в Build Settings у группы Asset Catalog Compiler Options в полях Widget Background Color Name и Global Accent Color Name соответственно.



Некоторые параметры могут быть скрыты (или показаны) в зависимости от выбранного значения в другом параметре через настройку Relationship.
Стоит отметить, что UI для редактирования параметра зависит от его типа. К примеру, если укажем Boolean, то мы увидим UISwitch, а если Integer, то тут у нас уже выбор из двух вариантов: ввод через UITextfield или пошаговое изменение через UIStepper.



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


Связку настроили, осталось определить, откуда сам интент возьмет реальные данные. Мостик с основным приложением в этом случае файл в общей группе (App Groups). Основное приложение пишет, виджет читает.
Для получения URL к общей группе используется следующий метод:
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group.ru.yourcompany.yourawesomeapp)

Сохраняем всех кандидатов, так как они будут использоваться пользователем в настройках как словарь для выбора.
Далее операционная система должна узнать, что данные обновились, для этого вызываем:
WidgetCenter.shared.reloadAllTimelines()// Или WidgetCenter.shared.reloadTimelines(ofKind: "kind")

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

Обновление данных


В целях бережного отношения к батарейке пользовательского девайса Apple продумали механизм обновления данных на виджете с использованием timeline механизма генерации слепков (snapshot). Напрямую разработчик не обновляет и не управляет view, но зато предоставляет расписание, руководствуясь которым, операционная система нарежет снапшотов в бэкграунде.
Обновление происходит по следующим событиям:
  1. Вызов используемого ранее WidgetCenter.shared.reloadAllTimelines()
  2. При добавлении виджета пользователем на рабочий стол
  3. При редактировании настроек.

Также в распоряжении разработчика три вида политик по обновлению таймлайнов (TimelineReloadPolicy):
atEnd обновление после показа последнего cнапшота
never обновление только в случае принудительного вызова
after(_:) обновление через определенный промежуток времени.

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

struct CardListProvider: IntentTimelineProvider {    public typealias Intent = DynamicMultiSelectionIntent    public typealias Entry = CardListEntry    public func placeholder(in context: Context) -> Self.Entry {        return CardListEntry(date: Date(), cards: testData)    }    public func getSnapshot(for configuration: Self.Intent, in context: Self.Context, completion: @escaping (Self.Entry) -> Void) {        let entry = CardListEntry(date: Date(), cards: testData)        completion(entry)    }    public func getTimeline(for configuration: Self.Intent, in context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> Void) {        let cards: [WidgetCard]? = configuration.cards?.compactMap { card in            let id = card.identifier            let storedCards = SharedStorage.widgetRepository.restore()            return storedCards.first(where: { widgetCard in widgetCard.id == id })        }        let entry = CardListEntry(date: Date(), cards: cards ?? [])        let timeline = Timeline(entries: [entry], policy: .never)        completion(timeline)    }}struct CardListEntry: TimelineEntry {    public let date: Date    public let cards: [WidgetCard]}

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

Отдельно стоит отметить показ виджета, если он находится в стэке из виджетов (Smart Stack). В этом случае для управления приоритетами мы можем воспользоваться двумя вариантами: Siri Suggestions или через установку значения relevance у TimelineEntry с типом TimelineEntryRelevance. TimelineEntryRelevance содержит два параметра:
score приоритет текущего снапшота относительно других снапшотов;
duration время, пока виджет остается актуальным и система может поставить его на верхнюю позицию в стэке.

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

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

Text поддерживает следующие стили:
relative разница времени между текущей и заданной датами. Тут стоит отметить: если дата указана в будущем, то начинается обратный отсчет, а после показывается дата от момента достижения нуля. Такое же поведение будет и для следующих двух стилей;
offset аналогично предыдущему, но есть индикация в виде префикса с ;
timer аналог таймера;
date отображение даты;
time отображение времени.

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

let components = DateComponents(minute: 10, second: 0) let futureDate = Calendar.current.date(byAdding: components, to: Date())! VStack {   Text(futureDate, style: .relative)      .multilineTextAlignment(.center)   Text(futureDate, style: .offset)      .multilineTextAlignment(.center)   Text(futureDate, style: .timer)      .multilineTextAlignment(.center)   Text(Date(), style: .date)       .multilineTextAlignment(.center)   Text(Date(), style: .time)      .multilineTextAlignment(.center)   Text(Date() ... futureDate)      .multilineTextAlignment(.center)}



Превью виджета


При первом отображении виджет будет открыт в режиме превью, для этого нам необходимо вернуть TimeLineEntry в методе placeholder(in:). В нашем случае это выглядит так:
func placeholder(in context: Context) -> Self.Entry {        return CardListEntry(date: Date(), cards: testData) }

После чего к view применяется модификатор redacted(reason:) с параметром placeholder. При этом элементы на виджете отображаются размытыми.



Мы можем отказаться от этого эффекта у части элементов, использовав unredacted() модификатор.
Также в документации сказано, что вызов метода placeholder(in:) происходит синхронно и результат должен вернуться максимально быстро, в отличие от getSnapshot(in:completion:) и getTimeline(in:completion:)

Скругление элементов


В гайдлайнах рекомендуется согласовать скругление у элементов со скруглением виджета, для этого в iOS 14 была добавлена структура ContainerRelativeShape, которая позволяет применить к view форму контейнера.

.clipShape(ContainerRelativeShape()) 

Поддержка Objective-C


В случае необходимости добавить в виджет код на Objective-C (например, у нас на нем написана генерация изображений штрихкодов) всё происходит стандартным способом через добавление Objective-C bridging header. Единственная проблема, с которой мы столкнулись при сборке Xcode перестал видеть автогенерируемые файлы интентов, поэтому мы также добавили их в bridging header:

#import "DynamicCardSelectionIntent.h"#import "CardSelectionIntent.h"#import "DynamicMultiSelectionIntent.h"

Размер приложения


Тестирование проводилось на Xcode 12 beta 6
Без виджета: 61.6 Мб
С виджетом: 62.2 Мб

Резюмирую основные моменты, которые рассмотрели в статье:
  1. Виджеты отличная возможность пощупать SwiftUI на практике. Добавляйте их в проект, даже если минимальная поддерживаемая версия ниже iOS 14.
  2. WidgetBundle используется для увеличения числа доступных виджетов, вот отличный пример как много различных виджетов имеет приложение ApolloReddit.
  3. Для добавления пользовательских настроек на самом виджете поможет IntentConfiguration или StaticConfiguration, если пользовательские настройки не нужны.
  4. Общая папка на файловой системе в общей группе App Groups поможет синхронизировать данные с основным приложением.
  5. На выбор разработчику предоставляется несколько политик обновления таймлайна (atEnd, never, after(_:)).

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

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

Каждый третий айтишник в России самоучка

11.09.2020 14:12:59 | Автор: admin
image

Привет, Хабр! В преддверии 256-го дня года мы решили выяснить, а откуда вообще берутся IT-специалисты (где их очень ждут, мы знаем и так смотрите вакансии). Так мы опросили больше 700 специалистов со всей страны и вот что выяснили.

image

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

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

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

image

image

IT по-прежнему остается сферой труда с доминирующим количеством мужчин лишь 21% опрошенных Ozon специалистов женщины (за год это распределение не изменилось, аналогичные результаты ранее публиковал портал HeadHunter). Средний возраст специалистов 22-28 лет.

В топ-3 популярных специальностей вошли разработка (3 из 4 опрошенных программисты), тестирование (6% опрошенных) и менеджмент проектов (чуть больше 2%), что также подтверждает статистика вакансий на рынке труда в IT.

Каждый пятый специалист пришел в IT, окончив курсы. При выборе программы лучше всего работает сарафанное радио большинство делает выбор, опираясь на рекомендации (63%). Немалую роль играют известность преподавателей больше половины опрошенных обращают внимание на преподавательский состав, а каждый третий на известность организатора. Возможность учиться бесплатно отметили лишь 28% респондентов, на гарантию трудоустройства при выборе курса обращают внимание только треть опрошенных, а вот возможность попасть в крупную компанию после обучения волнует только одного из 10.

А как вы попали в IT? Расскажите в комментариях. И с наступающим Днем программиста, конечно!)
Подробнее..

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

13.09.2020 18:19:09 | Автор: admin
На этой неделе Google выпустил Android 11, а Huawei представил Harmony 2.0, Apple продолжила биться с Epic в суде, мы продолжили исследование Kotlin в 1.4 и новых веяний неоморфизма, стагнации машинного обучения и правил создания иконок. Все это и многое другое в нашей новой подборке!


Упаковываю и отправляю приложение без троянов для управления своими лампами в F-Droid без каких-либо знаний в разработке для Android. Корпорация Google опубликовала релиз мобильной ОС Android 11. Главный акцент в новой версии операционной системы сделан на упрощении работы с различными мессенджерами, управлении smart-устройствами и улучшенной конфиденциальностью пользователя.

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

iOS

Apple подала встречный иск к Epic
Микровзаимодействия: анимированная волна
7 вариантов AlertView в SwiftUI
Тестирование производительности Xcode на большом проекте Swift. Сравнение iMac, MacBook, iMac Pro
Преобразование платного iOS-приложения в подписное
Пишем первый виджет для iOS
7 основных инструментов iOS-разработчика
Создание 3D анимации прокрутки карт в SwiftUI
Как сделать iOS-приложение безопасным?
SwiftUI 2.0: будущее декларативно
Swiftagram: клиент для Instagram
Velik: отслеживание поездок на велосипеде

Android

(+11) Navigation Component-дзюцу, vol. 1 BottomNavigationView
(+6) Полируем UI в Android: StateListAnimator
(+4) Превращаем EditText в SearchEditText
Google выпустил Android 11 Go
Huawei представил Harmony 2.0
JetBrains проводит конференцию по Kotlin 1.4
Android Broadcast: новый компилятор Kotlin в 1.4
20 инструментов Android-разработчика, о которых вы, вероятно, никогда не слышали
Google показал зависимость Firebase от GMS
Шесть лет споров: зачем Microsoft сделала Android-смартфон с двумя экранами в мире, где все устройства одинаковые
Подход чистой архитектуры при рассмотрении Модели
Не изобретайте колесо заново, делегируйте его!
Базовая инъекция зависимостей с помощью Hilt
Магические функции Kotlin все, что вам нужно знать
Полируем UI в Android: StateListAnimator
Сборка Android: как уменьшить время с 5 минут до 15 секунд
Разработка сложного пользовательского интерфейса с использованием Android ConstraintLayout
22 расширения Kotlin для более чистого кода
Простая библиотека настроек создаем экран настроек за секунды
TDD в Android
Современная безопасная Android-разработка
Неисправный AndroidX FragmentFactory
Исследуем Jetpack DataStore
Biometric Auth: биометрическая аутентификация в Kotlin
Blue Pair: работа с Bluetooth в Android

Разработка

(+25) Неоморфизм и его проблемы
(+11) Домофоны, СКУД И снова здравствуйте
(+10) Как захватить новую страну за 3 недели
(+6) Flutter.dev: Простое управление состоянием приложения
(+4) Локализуем приложение на React Native
Podlodka #180: PHP
C++ стал самым быстрорастущим языком программирования рейтинга TIOBE
Яндекс запускает новый сезон стажировок
Дизайн приложений: примеры для вдохновения #16
Мотивация разработчиков и других людей творческих профессий руководство для компаний
Илкка Паананен: Игры, как бизнес, не должны управляться процессами
Инструкция: как создать приложение для просмотра погоды на Flutter
Сетки, принципы и правила создания интерфейсных иконок, iOS и Android
Год на воде и хлебе: как делать приложение на свои и не сдаваться
5 советов по улучшению дизайна кнопок. Основы UI дизайна
Действительно ли Firebase так хорош, как кажется?
Создаем веб-приложение Flutter с нуля и размещаем его с помощью Continuous Deployment
4 типа разработчиков, с которыми вы (к сожалению) будете работать
Использование шаблона BLoC для чистых Flutter-приложений: теория и практический пример
Анатомия превосходного дизайна
Советы, как стать более эффективным ревьювером кода
Duofolio: ридер со словарем

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

(+21) Российские пасхалки в мобильных приложениях. Какие они?
(+17) Ошибки в дизайне A/B тестов, которые я думала, что никогда не совершу
(+4) Как понять, что новая фича принесет пользу продукту, а не навредит ему?
(+2) Apple Grace Period и Billing Retry статусы при обработке чеков пользователей
AppsFlyer запускает Xpend платформу для агрегации данных о расходах на рекламу
Руководство по продуктовой аналитике от Mixpanel
Самые скачиваемые приложения в августе 2020
Mustard: скаутинг на основе ИИ
Два типа стратегий роста: стратегии искры (kindle) и стратегии пламени (fire)
Как итерации помогают в поисковой оптимизации приложений

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

(+31) Стагнация машинного обучения. Многие задачи не будут решены никогда?
(+12) Автоматизируем работу системы отопления в квартире без переделки интерьера умный дом z-wave
(+9) Будни OEMщика (Часть 1)
(+3) Интернет автомобилей: первые шаги к беспилотной езде
Яндекс выпустит ТВ-приставку с Алисой
Представлен новый протокол Z-Wave Long Range
Как стать экспертом в области искусственного интеллекта: пошаговое руководство
Planet-Scale AR Alliance готовит дополненную реальность для 5G
Relativty VR-гарнитура с открытым исходным кодом за 200 долларов

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

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

20.09.2020 16:05:00 | Автор: admin
В этом дайджесте презентация Apple, инструменты и антипаттерны Android-разработки, ARM против x86 и кроссплатформа против нативной разработки, искусство рассказывания историй, секреты улучшения дизайна и многое другое!


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

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

iOS

(+22) О чем нам рассказали на ежегодной сентябрьской презентации Apple
(+5) Формальные грамматики на службе мобильного клиента
Подготовка к iOS 14
Что означает последнее обновление правил конфиденциальности Apple для вашего приложения
Apple представляет совершенно новый iPad Air с A14 Bionic, iPad 8-го поколения, Apple Watch Series 6 и Apple Watch SE
В App Store разрешили стриминг игр, но очень ограниченно
Социальная сеть на Swift UI
iOS 14 UISplitViewController: 5 проблем, с которыми вы можете столкнуться
Объяснение Диапазонов в Swift на примерах
Декодирование JSON в Swift с помощью Codable: практическое руководство
10 Pod-ов для использования в новом iOS-проекте
Улучшите свой UX с помощью Core Animations
Как обезопасить iOS-приложение от скриншотов и записи экрана?
WidgetKit: продвинутая разработка
10 мощных@Атрибутов в Swift
DTTextField: поле ввода с подсказками
ContainerController: выезжающая панель

Android

(+15) 20 инструментов Android-разработчика, о которых вы могли не знать
(+8) Navigation Component-дзюцу, vol. 2 вложенные графы навигации
(+2) Антипаттерн Репозиторий в Android
(+1) Обзор HMS Core 5.0: ещё больше возможностей для ML на мобильных устройствах и новые инструменты для аудио и видео
(0) Как с помощью возможности распознавания текста HUAWEI ML Kit реализовать функцию автоматического ввода номеров
Microsoft запускает Android-приложения в Windows 10
Android 11 вызвал проблемы в работе с Android Auto
Привет DataStore, пока SharedPreferences
Объяснение жизненного цикла Android Fragment
Изучение Jetpack Compose: модификатор отступа
Управление несколькими приложениями в одном проекте Android (Studio)
Распознавание жестов поворота в Android
Как определить обновление Android-приложения
Просто добавьте MVI с Orbit 2
Адаптируйте свое приложение к последним рекомендациям по обеспечению конфиденциальности
Как корутины формируют новые способы разработки
Автоматизация Code Review
Почему я решил написать свой собственный инструмент для тестирования UI
Понимаем внутреннее устройство Lottie рендеринг файла анимации
JetInstagram: Instagram на Jetpack Compose

Разработка

(+19) ARM против x86: В чем разница между двумя архитектурами процессоров?
(+9) Когда имеет смысл писать кроссплатформенные приложения: появление и исчезновение React Native в Lingualeo
(+8) Вставка реальных объектов в Unity с помощью Meshroom
(+7) UXD Реальность и будущее в дизайне или человек во главе всего
(+7) Crash-crash, baby. Автоматический мониторинг фатальных ошибок мобильных приложений
(+3) Как документ на мобильнике распознается: от простого к сложному
Podlodka #181: хантинг
Искусство рассказывания историй в разработке программного обеспечения
Дизайн приложений: примеры для вдохновения #17
Секрет улучшения дизайна: 4 способа сторителлинга
Исследование. Какую иконку выбрать для обозначения аккордеонов?
Искусство сторителлинга в разработке программного обеспечения
Руководство по минималистическому дизайну
Автоматизация публикации ваших приложений Flutter в Google Play с помощью GitHub Actions
Создаем приложения для чата на Flutter с помощью Firebase
Жизненный цикл разработки программного обеспечения: как мы создали новый Dropbox Plus
Барьеры на пути к разработке игр устранены
12 основных инструментов для разработчика мобильных приложений на Flutter
Начинаем работать с дополненной реальностью с помощью Unity AR Foundation Framework
Действительно ли я знаю программирование?
Mixin: мессенджер, кошелек и клиент для децентрализованной сети

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

(+1) Как представить игру издателям и инвесторам
Почему следующая фаза роста Китая будет определяться потребителями и что это означает для рекламодателей
Bunch получил $20 млн. на социальный слой для игр
Министерство финансов США изучает безопасность игр Riot Games и Epic Games
make sense: О выборе фреймворков приоритизации, подходах к принятию решений и командной осознанности
Зачем бизнесу заказывать разработку приложения?
Отчет О состоянии рынка рекламы приложений для шоппинга в 2020 году
Как студия Donut Lab закрыла раунд инвестиций на $1.6M
Маркетинг приложений в апокалипсис: как работать с тревожными трендами?
Как я получил 200 000 загрузок приложений без платного маркетинга

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

(+29) Подключем новый Xiaomi Gateway 3 к Home Assistant без паяльника и смс
(+24) Как за два месяца пройти путь от начинающего питониста до сертифицированного TensorFlow-разработчика
(+12) ИК датчик движения на STM32
(+3) Автомобильное ПО: варианты стратегического развития
Facebook анонсировал Oculus Quest 2
Facebook выпустит смарт-очки вместе с Ray-Ban
Gameloft оживляет игрушки Kinder с помощью дополненной реальности
Nvidia покупает ARM
8 лучших No-Code платформ машинного обучения, которые вы должны использовать в 2020 году

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

Локализуем приложение на React Native

11.09.2020 02:18:44 | Автор: admin
В ходе разработки одного из наших приложений нам понадобилось сделать поддержку мультиязычности. Задача была дать пользователю возможность менять язык(русский и английский) интерфейса приложения. При этом текста и контент должны переводиться на лету.

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

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

Определяем текущий язык устройства


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

import {NativeModules, Platform} from 'react-native';let deviceLanguage = (Platform.OS === 'ios'        ? NativeModules.SettingsManager.settings.AppleLocale ||          NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13        : NativeModules.I18nManager.localeIdentifier

Для ios мы извлекаем язык приложения через SettingsManager, а для android через нативный I18nManager.

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

Переводим на лету


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

И так мы должны создать класс(мне нравится называть модель), который будет отвечать за глобальное состояния текущей локализации. Создаем:

// ключ локального хранилища, в котором будем записывать текущий langconst STORE = '@lang-store';// список русскоязычных стран const RU_LANGS = [  'ru',  'az',  'am',  'by',  'ge',  'kz',  'kg',  'md',  'tj',  'tm',  'uz',  'ua',];class LangModel {  @observable  lang = 'ru'; // по умолчанию  constructor() {    this.init();  }  @action  async init() {    const lang = await AsyncStorage.getItem(STORE);    if (lang) {      this.lang = lang;    } else {      let deviceLanguage: string = (Platform.OS === 'ios'        ? NativeModules.SettingsManager.settings.AppleLocale ||          NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13        : NativeModules.I18nManager.localeIdentifier      ).toLowerCase();      if (        RU_LANGS.findIndex((rulang) => deviceLanguage.includes(rulang)) === -1      ) {        this.lang = 'en';      }      AsyncStorage.setItem(STORE, this.lang);    }}export default new LangModel();


При инициализации нашей модели мы вызываем метод init, который берет локаль либо из AsyncStorage, если там есть, либо извлекаем текущий язык устройства и кладет в AsyncStorage.

Далее нам нужно написать метод(action), который будет менять язык:

  @action  changeLang(lang: string) {    this.lang = lang;    AsyncStorage.setItem(STORE, lang);  }


Думаю, что тут все понятно.

Теперь самое интересное. Сами переводы мы решили хранить простым словарем. Для этого создадим js файл рядом с нашей LangModel, в котором мы поместим наши переводы:

// translations.js// Да, за основу мы взяли русский. export default const translations = {  "Привет, Мир!": {en: "Hello, World!"},}


Далее реализуем еще один метод в LangModel, который будет принимать на вход текст и возвращать текст текущей локализации:

import translations from './translations';  ...  rk(text) {    if (!text) {      return text;    }    // если локаль ru, то переводить не нужно    if (this.lang === 'ru') {      return text;    }    // если перевода нет, кинем предупреждение     if (translations[text] === undefined || translations[text][this.lang] === undefined) {      console.warn(text);      return text;    }    return translations[text][this.lang];  }


Все, наш LangModel готов.

Полный код LangModel
import {NativeModules, Platform} from 'react-native';import {observable, action} from 'mobx';import AsyncStorage from '@react-native-community/async-storage';import translations from './translations';const STORE = '@lang-store';// список ru локали const RU_LANGS = [  'ru',  'az',  'am',  'by',  'ge',  'kz',  'kg',  'md',  'tj',  'tm',  'uz',  'ua',];class LangModel {  @observable  lang = 'en';  constructor() {    this.init();  }  @action  async init() {    // Берем текущую локаль из AsyncStorage    const lang = await AsyncStorage.getItem(STORE);    if (lang) {      this.lang = lang;    } else {      let deviceLanguage: string = (Platform.OS === 'ios'        ? NativeModules.SettingsManager.settings.AppleLocale ||          NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13        : NativeModules.I18nManager.localeIdentifier      ).toLowerCase();      if (        RU_LANGS.findIndex((rulang) => deviceLanguage.includes(rulang)) > -1      ) {        this.lang = 'ru';      }      AsyncStorage.setItem(STORE, this.lang);  }  @action  changeLang(lang: string) {    this.lang = lang;    AsyncStorage.setItem(STORE, lang);  }  rk(text) {    if (!text) {      return text;    }    // если локаль ru, то переводить не нужно    if (this.lang === 'ru') {      return text;    }    // если перевода нет, кинем предупреждение     if (translations[text] === undefined || translations[text][this.lang] === undefined) {      console.warn(text);      return text;    }    return translations[text][this.lang];  }}export default new LangModel();



Теперь мы можем использовать метод rk для локализация текста:

<Text>{LangModel.rk("Привет, Мир!")}</Text>


Посмотреть как это работает можно в нашем приложении в AppStore и Google Play (Нажать на иконку (!) справа вверху, пролистать вниз)

Бонус


Конечно, писать каждый раз LangModel.rk это не круто. Поэтому мы можем создать собственный компонент Text и в нем уже использовать LangModel.rk

//components/text.jsimport React from 'react';import {Text} from 'react-native';import {observer} from 'mobx-react';import {LangModel} from 'models';export const MyText = observer((props) => (   <Text {...props}>{props.notTranslate ? props.children : LangModel.rk(props.children)}</Text>));


Так же нам может понадобиться, например, менять логотип приложения в зависимости от текущей локализации. Для этого можно просто менять контент в зависимости от LangModel.lang (не забудьте обернуть ваш компонент в observer(MobX))

P.S.: Возможно такой подход вам покажется не стандартным, но он нам понравился больше чем то, что предлагает react-native-i18n

На этом у меня все. Всем спасибо!)
Подробнее..

Обзор HMS Core 5.0 ещё больше возможностей для ML на мобильных устройствах и новые инструменты для аудио и видео

14.09.2020 18:23:30 | Автор: admin


Привет, Хабр! Вместе с Harmony OS мы представили пятую версию HMS Core набора инструментов, с помощью которых можно разрабатывать приложения для экосистемы Huawei. Мы добавили новые возможности для работы с контентом, сделали акцент на безопасности данных, взаимодействии между устройствами и расширили возможности для AI-инструментов обо всём этом мы детально поговорили на нашей конференции HDC.Together, а в этой статье дадим обзор новых возможностей HMS.

Инструменты для работы с ML и AI


Основные сервисы для работы с AI входят в ML Kit и позволяют работать с текстом, голосом, картинками, AR/VR-технологиями. В HMS Core 5.0 мы увеличили количество поддерживаемых языков до 50 и можем выполнять перевод между 20 языками, при этом на вход принимаются как текстовые записи, так и голосовые. Также ML Kit может быть использован для отсеивания спама и всяких неприличных картинок.

С ML Kit можно конвертировать голосовые записи в текстовый формат и обратно, и мы расширили этот функционал с помощью Video Course Creator, который автоматически создаёт образовательные курсы: он берёт на вход образовательную программу, комментарии учителя и предустановленные аудио- и видеопараметры во время трансляции учитель может обращаться к нужным материалам и все записанные материалы компонуются в единый курс.



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

Работать с инструментами AI и ML можно на разных уровнях:
  • Платформа для работы с технологиями искусственного интеллекта на мобильных устройствах HiAI позволяет обучать нейросети, создавать модели и конвертировать их в бинарный файл, который уже можно загрузить на NPU-чип.
  • Платформа Ability Gallery предоставляет разработчикам готовые сценарии использования AI и позволяет работать с большими данными в своих приложениях.

AR/VR


Наш AR-движок анализирует информацию об освещении, плоскости, форме объектов, типе поверхности, умеет искать заданные объекты в пространстве. Отдельно система может строить 3D-схему с помощью опорных точек и отслеживать человеческие движения, жесты и мимику. Например, для определения положения руки выделяется 21 точка, а для положения тела 23 точки. Сейчас система может распознавать 6 поз и анализировать сразу 2 человек.


С пятой версии HMS Core CameraKit обеспечивает различные режимы съёмки: широкую диафрагму, портретный режим, HDR, размытие фона, суперночной режим и иже с ними. Также появилась возможность использовать AI в фото- и видеосъемке для предварительного выбора фильтров и цветокоррекции.

Совместная работа устройств


В новой версии HMS Core появилось несколько новых движков для совместной работы разных устройств. Мы упростили взаимодействия между устройствами и умными вещами с помощью DeviceVirtualization-движка. Он преобразует периферийные устройства в виртуальные объекты и позволяет телефону работать с ними так же, как с обычными функциями.

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

Ещё один новый движок OneHop Kit работает с NFC и позволяет безопасно передавать данные между устройствами в одно касание. С его помощью можно синхронизировать устройства Huawei между собой, передавать файлы и открывать приложения на других устройствах с теми же настройками, что и на основном. Пока он работает только между телефонами и планшетами Huawei, поэтому для связи с другими устройствами мы предоставляем Share-движок, который обеспечивает скорость до 80 Мб/c по Bluetooth.

Видео и аудио



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

В Audio Kit теперь есть аудиодвижок для записи звука в высоким качестве с функциями оптимизации задержки и других инструментов. Видеодвижок, в свою очередь, поддерживает основные протоколы HTTP, HTTPS, HLS, DASH. Также он позволяет организовывать стриминг со сторонних сервисов с помощью Video Kit WisePlayer SDK.

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

Картинки и 3D-рендеринг


Для обработки изображений появился Image Kit он предлагает более 20 фильтров и визуальных эффектов, включая анимацию с рендерингом. В него входят 2 SDK: Image Vision SDK для работы с цветовыми фильтрами и Image Render SDK для использования эффектов анимации.

Scene Kit предназначен для работы с 3D-объектами: он использует метод физически корректного рендеринга (PBR) и позволяет приложениям подключаться через API и получать 3D-модели сложных объектов. Движок предлагает три сценария работы: SceneView для общих сцен (не-AR), ARView для общих сцен AR и FaceView для работы с лицами в сценах AR.

Аналитика и безопасность


В Сore 5.0 Huawei мы запустили систему тегов Dynamic Tag Manager (DTM) для отслеживания маркетинговой активности пользователей: она интегрируется как с самими сервисами Huawei, так и со сторонними платформами для отправки и обработки данных. С помощью DTM можно динамически обновлять теги в пользовательском веб-интерфейсе, отслеживать определённые события и отправлять данные на сторонние аналитические платформы. В наших системах главный акцент сделан на безопасность, поэтому движок DTM также используется как antifraud-система для отслеживания подозрительной активности.

Одной из фишек новой версии HMS стала аутентификация по лицу с помощью LocalAuthentication Engine. Он работает с инфракрасной камерой, которая строит модель по опорным точкам и производит аутентификацию с помощью ML Kit.

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



Где узнать подробности?


Все записи конференции HDC.Together доступны по ссылке. Здесь можно послушать доклады о новинках платформы, основных сценариях работы с инструментами HMS и задать технические вопросы на Huawei Developer Forum или Stackoverflow c тегом huawei-mobile-services.
Подробнее..

Когда имеет смысл писать кроссплатформенные приложения появление и исчезновение React Native в Lingualeo

15.09.2020 10:12:59 | Автор: admin

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

Мы попросили лидера мобильной разработки Артёма Рыжкина (phoenix_rav) рассказать о том, откуда в нативных приложениях Lingualeo появились модули на React Native, какие они вызывали проблемы и когда вообще имеет смысл делать кроссплатформенные приложения.

Как в приложении Lingualeo появились кроссплатформенные элементы

Приложение Lingualeo появилось в русских сторах в 2012 году. Тогда ещё не было кроссплатформенных технологий, приложение для iOS написали на Objective-C, а для Android на Java. Их поддерживали и развивали две отдельные команды.

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

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

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

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

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

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

К тому же в тот момент веб переезжал на React. В Lingualeo была сильная веб-команда, и на это сделали ставку: решили, что разработчикам, знакомым с React, будет комфортнее и быстрее разобраться с React Native, чем с другой технологией. Шесть игровых тренировок сначала сделали на вебе, потом портировали в приложения.

В итоге когда я пришёл в команду в 2018 году, то увидел интересную ситуацию. Как минимум я с таким раньше не сталкивался: в компании нативные приложения на iOS и Android, но в них есть шесть тренировок, написанных на стороннем фреймворке, языке React Native. Получается, приложения по большей части нативные, но в чём-то кроссплатформенные.

Почему кроссплатформенные элементы в нативных приложениях это проблема?

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

React Native не позволяет полностью отказаться от нативного кода, заменив его кроссплатформенным. Для некоторых функций, например, проигрывания музыки, обращения к сенсорам устройства или к локальному хранилищу придётся писать так называемые bridge-классы.

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

У нас использовались bridge-классы код на двух нативных языках: Java/Kotlin для Android и Objective-C/Swift для iOS. Мы написали их для обращения к медиаплееру, к аналитическим системам, локальному хранилищу и логике авторизации.

Например, вот как bridge-класс запускает Intent Service в Android:

public class LLMergeBridgeModule extends ReactContextBaseJavaModule {

protected static final String MODULENAME = "LLMergeBridgeModule";

@Override

public String getName() {

return LLMergeBridgeModule.MODULENAME;

}

public LLMergeBridgeModule(ReactApplicationContext reactContext) {

super(reactContext);

}

@ReactMethod

public void merge() {

ReactApplicationContext context = getReactApplicationContext();

SyncService.startServiceForceSync(context);

}

}

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

В нативных платформах со временем меняется логика работы: если в команде только разработчик на React Native, ему придётся следить ещё за двумя платформами. Когда выходят новые версии Android или iOS, нужно переписывать bridge-классы, тратить дополнительное время на поддержку. Разработчику нужно следить не только за развитием кроссплатформенного фреймворка, на котором он работает, но и за обновлениями обеих мобильных платформ.

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

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

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

Например, на момент внедрения React Native у нас уже было два проекта для локализации строк в системе переводов. Один работал для приложения на Android, другой для iOS. Каждый из проектов учитывал особенности своей платформы.

При подключении React Native нужно было завести третий проект, но тогда бы сложилась ситуация дублирования строк: одна и та же строка была бы заведена в 2 проекта сразу. Это бы потребовало дополнительного труда переводчиков. Поэтому мы решили использовать маппер для преобразования строк из iOS-проекта в формат для React Native.

С августа 2019 все приложения Google Play должны обязательно поддерживать 64-битную архитектуру. Если для нативного приложения такая поддержка включалась парой строчек кода, то для React Native пришлось мигрировать на последнюю версию. Нативный разработчик несколько недель разбирался в коде, чтобы адаптировать его под новую версию.

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

Как мы вернулись к полностью нативным приложениям

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

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

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

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

Дополнительный плюс от перехода на полностью нативные приложения оказался в том, что размер архива приложения уменьшился на 28 Мб. Также сократилось время сборки: раньше билд релиза Android-приложения на Mac mini занимал до 20 минут на холодном старте, а сейчас 2.

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

В каких случаях выгодно делать кроссплатформенное приложение?

React Native вполне подойдёт для многих проектов. Но, по нашему мнению, его выгоднее всего использовать, если ваша задача одновременно соответствует шести критериям:

1. Это простое приложение. Такое приложение не требует реализации сложной бизнес-логики и сложной UI-логики. Например, витрина интернет-магазина: приложение должно просто отобразить список товаров, дать возможность положить товар в корзину и оплатить покупку.

2. Это приложение должно быть одинаковым на каждой платформе. Если ваше приложение выглядит и построено так, что не будет отличаться в зависимости от платформы, используйте React Native. В таком случае получится сэкономить деньги бизнеса и время разработчиков за счёт унификации кода.

4. Это приложение не работает с медиафайлами, сенсорами и навигацией. React Native удобная технология для мобильных приложений, которым не требуется работать с медиафайлами и множеством сенсоров смартфона.

5. Вам важно быстро запуститься и вносить изменения на лету. Кроссплатформенные технологии отличный вариант для быстрого запуска. React Native будет выгодно использовать, например, стартапам. Скорее всего, кроссплатформенная разработка потребует меньше человеко-часов и предприниматель быстрее получит продукт. Его можно тестировать, показывать инвесторам и при необходимости менять код на лету без пересборки проекта, используя механизм code push.

6. У вас пока нет нативных приложений. Добавление кроссплатформенности в нативные приложения приведёт к дублированию сетевого слоя, логики работы UI, локализации, также появятся проблемы с настройками проекта.

Когда кроссплатформенные решения не выигрывают у нативных?

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

В приложении нужны сложные механики с медиа-ресурсами. React Native не справится с проигрыванием медиа без bridge-классов. Если в приложении будут многоступенчатые механики, связанные, например, с воспроизведением музыки, то реализовывать это на React Native может быть сложнее, чем в нативных приложениях.

В Lingualeo такие механики как раз есть в игровых тренировках. Например, в аудиотренировке текст сначала проигрывается до конца, затем разбивается на фрагменты. Пользователь может воспроизводить их и перетаскивать при помощи drag and drop.

Приложение будет много обращаться к сенсорам. Чтобы обратиться, например, к GPS, Bluetooth, акселерометрам, датчику лица или микрофону, коду на React Native также нужны bridge-классы.

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

Фичеринг приложения и следование гайдлайнам. Если вы хотите активно получать фичеринг от Google Play и Apple Store, то, скорее всего, от команды разработчиков потребуется внедрение новой функциональности, специфичной для каждой из двух платформ. Поддержка подобных фичей в React Native потребует времени со стороны разработчиков, а сторонний node module может появиться с заметным опозданием.

Выводы

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

  • не имеют нативной кодовой базы: то есть, сразу создаются как кроссплатформенные;

  • основаны на простой логике: им не нужен доступ к медиафайлам и датчикам;

  • не имеют встроенных покупок, например, платного премиум-доступа или игровой валюты;

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

Подробнее..

Crash-crash, baby. Автоматический мониторинг фатальных ошибок мобильных приложений

15.09.2020 12:21:41 | Автор: admin

Всем привет! Меня зовут Дмитрий, я релиз-инженер вкоманде CI/CD Speed Авито. Вот уже несколько лет мы сколлегами отвечаем за всё, что связано срелизами наших мобильных приложений и не только. Впрошлый раз я рассказывал онашей системе релизов мобильных приложений наоснове контракта. Сегодня речь пойдет отом, как мы автоматизировали сбор информации изFirebase оновых фатальных ошибках вмобильных приложениях.



Проблематика


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


Раньше, как и многие нарынке мобильных приложений, мы использовали Fabric, длякоторого vadimsmal и YourDestiny написали очень удобный клиент Fabricio. Набазе этого клиента унас была создана система мониторинга, которая заводила Jira-задачи нановые фатальные ошибки, искала ответственных поGit-Blame и сообщала обошибках вcпециальный слак-канал.


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


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


Получаем данные


Google Cloud Functions


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


Исследование документации Firebase привело нас кGoogle Cloud Functions или же облачным функциям. Это serverless FaaS отGoogle, который позволяет запускать ваш код воблачной инфраструктуре Google. УFirebase-Crashlytics есть встроенная интеграция соблачными функциями (намомент написания статьи данная функциональность помечена как deprecated). Вы можете написать call-back наодин изтрёх crashlytics-ивентов и дальше обрабатывать его как вашей душе угодно. Особенно нас интересуют два ивента onNew(новое событие crashlytics) и onVelocityAlert (резкий рост события crashlytics).



Вголове сразу же родилась схема. Настраиваем интеграцию Firebase-Google Cloud Functions, шлём оттуда все новые краши сразу всвой сервис, и там уже обрабатываем. Берём пример издокументации, вносим несколько доработок и получаем следующий код наJS который загружаем вGoogle Cloud:


const functions = require('firebase-functions');const rp = require('request-promise');function sendEvent(event) {    return rp({        method: 'POST',        uri: functions.config().crashlytics.crash_collector_url,        body: event,        json: true,    });}exports.NewIssueEvent = functions.crashlytics.issue().onNew(async (issue) => {    await processEvent(issue, 'NewIssueEvent')});exports.RegressedEvent = functions.crashlytics.issue().onRegressed(async (issue) => {await processEvent(issue, 'RegressedEvent')});exports.VelocityAlertEvent = functions.crashlytics.issue().onVelocityAlert(async (issue) => {await processEvent(issue, 'VelocityAlertEvent')});const processEvent = async (event, type) =>{    if (isActualEvent(event)) {        await sendEvent(event);        console.log(`Posted ${type} ${event.issueId} successfully to crash collector`);    }    else{        console.log(`It's old event or not Avito. Do nothing`);    }}const isActualEvent = (event) =>{    const {appInfo} = event;    const {appName, latestAppVersion} = appInfo;    const version = latestAppVersion &&  parseFloat(latestAppVersion.split(' ')[0]);    console.log(`Event appName: ${appName} version: ${version}`);    return appName === 'Avito' && version > 60.0}

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


Но втекущей реализации нам не хватает данных. ВFirebase-Crashlytics есть fatal события (собственно фатальные ошибки-краши) и non-fatal (остальные события которые по той или иной причине логируются в crashlytics). Все летящие кнам ивенты насобытие onNew не имеют признака фатальности, ктому же нам хотелось как-то фильтровать события поколичеству затронутых пользователей и частоте возникновения, но этой информации всобытиях нет.


BigQuery


Google позволяет экспортировать данные изFirebase вBigQuery. BigQuery облачное хранилище, предоставляющее удобную платформу дляхранения и обработки данных. На момент исследования всередине 2019года был доступен только один тип синхронизации cFirebase Batch Table.


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


  1. Синхронизация происходит раз всутки, приэтом нет гарантии, когда она будет завершена.
  2. Нельзя настроить тип экспортируемых событий экспортируется и fatal и non-fatal.
  3. Чем дольше живёт таблица, тем больше вней данных (ваш кэп) и тем дороже стоят услуги хранения.

Дорабатываем изначальную схему:



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


SELECT issue_id, is_fatal, COUNT(*) as crashes_counter, COUNT(DISTINCT installation_uuid) AS affected_users FROM `android.firebase_crashlytics.{table}` WHERE issue_id in ( {issues_id_string} ) GROUP BY issue_id, is_fatal LIMIT 1000

Внимательный читатель может заметить, что тут образовывается временной лаг между фактическим появлением краша и получением нами информации онём. Чтобы не пропускать редкие, но действительно важные краши, которые резко растут и задевают сразу много пользователей, унас по-прежнему оставалось событие onVelocityAlert вGoogle Cloud Function. Подокументации это событие вызывается исключительно нафатальные ошибки вработе приложения, если ошибка привела ксбою N сеансов пользователей запоследний час. Пофакту же onVelocityAlert не работало, мы зарепортили это вGoogle, нас внесли вовнутренний трекер, и наэтом всё.


Слак


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


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


Новая схема выглядела так:



Обрабатываем данные


Систочником данных вроде определились. Теперь нужно эти данные обрабатывать.


Напомню, что наша старая система наFabric делала сданными окрашах:


  1. Искала ответственного поGit-Blame.
  2. Создавала задачу наисправление.
  3. Оповещала оновом событии в специальный слак-канал.

Первое отчего мы решили отказаться это автоматическое создание задачи и поиск ответственного поGit-Blame. Поопыту, автоматически созданные задачи отправлялись накладбище Jira, и кним редко кто возвращался, а поиск поGit-Blame иногда давал сбой, что ещё больше повышало шансы забыть задачу. А вот оповещения вслак мы решили развивать, этот канал коммуникации показал себя наиболее эффективным.


Обработку решили реализовать набазе сервиса мобильных релизов Nupokati. Он собирает информацию поновым крашам, раз вдень покрону запрашивает дополнительные данные изBigQuery, фильтрует краши пофатальности и частоте возникновения нас не интересуют единичные сбои и отправляет daily report вслак поактуальной версии приложения.



Пример daily report


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


Помимо daily report мы отлавливаем VelocityAlert дляактуальной версии и тут же репортим опожаре вслак-канал и ответственному законкретный релиз инженеру. Втреде определяется, насколько взрыв фатален, и что сним делать.



Google Cloud Functions всё


Около года мы успешно эксплуатировали новую систему автоматического сбора и алертинга фатальных ошибок вмобильных приложениях. Уже практически забыли, как заходить вFirebase и смотреть краши. Как вдруг было объявлено, что интеграция Firebase-crashlytics и Google Cloud Functions deprecated и её работа будет приостановлена 1октября 2020года. Нужно было оперативно дорабатывать решение и отказываться отоблачных функций. Приэтом хотелось обойтись минимальными изменениями вработающей системе.



Так мы просто убрали Cloud Functions и доработали запрос наполучения данных изBigQuery. Вся остальная система осталась прежней: daily report, velocityAlerts, фильтры поколичеству задетых пользователей и слак-каналы. Новый запрос получает сразу все уникальные краши понужной версии и отправляет их впоток обработки.


SELECT issue_id, issue_title, is_fatal, COUNT(issue_id) as crashes_counter, ARRAY_AGG (distinct application.display_version) AS versions, COUNT(DISTINCT installation_uuid) AS affected_users FROM `android.firebase_crashlytics.{table}`WHERE is_fatal=true GROUP BY issue_title, issue_id, is_fatal HAVING ARRAY_LENGTH(versions)=1 AND "{version}" in UNNEST(versions)ORDER BY crashes_counter DESC

Итоги


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


Несколько советов тем, кто захочет повторить наш путь:


  • Использование BigQuery платное, но есть песочница, вкоторой можно поэкспериментировать.
  • Оптимизируйте запросы кBigQuery. Процессинг данных не бесплатный, он впрямом смысле имеет денежное выражение согласно тарифам.
  • Дляоптимизации затрат нахранение данных вBigQuery уменьшайте время жизни таблиц, это есть внастройках. Длянас оптимальным отказался период жизни таблицы впять дней.
  • Уже после создания нашей системы появился BigQuery streaming. Нанём можно собрать аналогичную систему или даже лучше.
  • Внимательней читайте документацию кGoogle Cloud Platform. Это очень мощная платформа смножеством инструментов и возможностей.
Подробнее..

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

17.09.2020 10:15:44 | Автор: admin

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

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

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

Принципы

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

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

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

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

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

Трудности

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

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

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

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

Поиск документа

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

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

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

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

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

3. Наиболее общий подход, основанный на связке: особые точки + дескрипторы + RANSAC. Вначале на входном изображении производится поиск особых точек (мы используем модификацию особых точек YAPE для изображений с большим разбросом локального контраста, про эту модификацию можно почитать в этом докладе) и для каждой точки вычисляется локальный дескриптор (в нашем случае - методом RFD, модифицированным с целью ускорения). Далее в индексе известных дескрипторов запуском серии поисков ближайших соседей выявляются шаблоны-кандидаты. После этого кандидаты верифицируются при помощи метода RANSAC с учетом геометрического расположения особых точек. Этот метод используется в Smart IDReader для поиска и типизации подавляющего числа типов идентификационных документов. Подробнее про него можно почитать в этом докладе.

Поиск полей

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

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

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

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

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

Обработка и распознавание полей

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

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

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

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

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

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

Использование нескольких кадров

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

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

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

Последним по порядку, но не по значению, аспектом работы с видеопотоком является анализ голограмм и других OVD (Optical Variable Devices) - элементов защиты документов, которые меняют свой внешний вид при изменении освещения или угла обзора. Для того, чтобы их детектировать и верифицировать их наличие, последовательность кадров просто необходима по определению. Рассматривая только одно изображение, верифицировать наличие такого элемента защиты нельзя.

Детали, детали

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

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

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

Подробнее..

Как мы автоматизировали разработку WL-приложений

21.09.2020 12:22:00 | Автор: admin
White Label это мобильные приложения, которые можно кастомизировать под любой бренд: оформить в фирменных цветах, выбрать необходимые блоки и функционал, добавить описание. Мы их выпускаем на основе Рамблер/кассы с 2015 года и в этой статье хотим рассказать, как у нас получилось автоматизировать и ускорить разработку WL.



Рамблер/касса онлайн-сервис и приложение для продажи билетов на концерты, в театр, кино, спортивные и другие мероприятия. Также мы разрабатываем другие B2B-предложения для партнеров: мобильный SDK, встроенные виджеты для сайтов и соцсетей, CRM-систему для аналитики продаж и аудитории, а также ряд технологических решений. Но сегодня мы остановимся только на WL.

Что было раньше


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



В идеальных условиях всего на разработку нового WL должно уходить минимум 2-3 дня по одному на разработчиков iOS и Android, плюс тестирование. Если сотрудник первый раз сталкивается с созданием WL-приложения, то выполнение задачи у него может занять до трех дней, что увеличивает общее время работы. Таким образом, для Рамблер/кассы создание WL-приложения это рутинная задача, которая ложилась на плечи разработчиков и отнимала у них ценное время.

Какие были варианты решений


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

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

Как работает решение


Мы запустили сайт с административной панелью в виде микрослужб новый раздел в существующей админке (CMS) по управлению продажами и витринами Рамблер/кассы. Мы разработали скрипты для iOS и Android, которые локально создают в проекте новое приложение и подают на него все нужные параметры.

В качестве брокера сообщений используется RabbitMQ, а все настройки сохраняются в архив и публикуются в рамблеровский Artifactory. После этого используется API GitLab, чтобы запустить процесс сборки в мобильных репозиториях.

На стороне бэкенда формируется архив с файлами в формате JSON, содержащими информацию, которую ввели в административной панели, и графикой. Триггер Gitlab CI вызывает pipeline, в параметрах к которому передает ссылку на архив из Artifactory. Скрипт, настроенный на билд машине и лежащий в корне проекта запускается с входным параметром-ссылкой.

Скрипт для iOS

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

Скрипт для Android

Джоба подтягивает нужные библиотеки jq и unzip. Библиотека unzip распаковывает архив, скрипт парсит json с помощью jq, создает новую папку в app модуле и проверяет наличие .jks файла для данного приложения.

Если приложение новое, то создается данный файл, после собирается релизное APK и скрипт отправляет в его firebase обновляет приложение в Маркете. Далее задача проверяет появился ли новый .jks файл, и, если он есть, то пушит его в GitLab.

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

На практике автоматизация создания WL-приложений выглядит так


  1. Партнёр Рамблер/кассы заполняет и передает заполненный единообразный бриф и брендбук компании, в которых собраны все тексты, изображения, шрифты, иконки, контактные данные и параметры будущего приложения.
  2. Менеджер проекта или сотрудник поддержки формирует и уточняет требования.
  3. Дизайнер на основе брендбука или сайта партнёра предлагает свое решение по цветам и иконкам.
  4. В админке менеджер проекта самостоятельно заполняет все нужные параметры для нового приложения (ID сервисов, цвета, конфигурационные файлы, иконки и т.д.).
  5. После заполнения необходимых параметров менеджер проекта нажимает кнопку Создать приложение, а затем готовая сборка передается на тестирование.
  6. Тестировщик тестирует приложение и публикует его в App Store и Google Play с помощью CI.



Что получилось


Мы максимально автоматизировали создание WL-приложений. Раньше сам процесс разработки занимал 2-3 дня и отнимал ресурсы программистов, а теперь менеджер за 15 минут вбивает все данные и через примерно 20 минут сборка автоматически создается и передается QA на тестирование. Наши партнёры получают все возможности, которые есть в Рамблер/кассе, а мы экономим время, ресурсы и минимизируем ошибки.
Подробнее..

Перевод Flutter.dev Continuous delivery с Flutter

22.09.2020 18:18:39 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса Flutter Mobile Developer.





Следуйте лучшим практикам непрерывного развертывания (continuous delivery CD) вместе с Flutter, чтобы ваше приложение было непременно доставлено вашим бета-тестерам и проверялось на регулярной основе без необходимости прибегать к ручным манипуляциям.

fastlane


В этом руководстве показано, как интегрировать fastlane (набор инструментов с открытым исходным кодом) в существующие рабочие процессы тестирования и непрерывной интеграции (continuous integration CI), например, Travis или Cirrus.

Local setup

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

  1. Установите fastlane: gem install fastlane или brew install fastlane. Для получения более развернутой информации посетите документацию Fastlane.
  2. Создайте проект Flutter и, когда он будет готов, убедитесь, что ваш проект собирается посредством
    • flutter build appbundle; и
    • flutter build ios --release --no-codesign.
  3. Инициализируйте проекты Fastlane для каждой платформы.
    • Запустите fastlane init в каталоге [project]/android.
    • Запустите fastlane init в каталоге [project]/ios.
  4. Убедитесь, что Appfileы содержат адекватные метаданные для вашего приложения.
    • Убедитесь, что package_name в [project]/android/fastlane/Appfile совпадает с именем вашего пакета в AndroidManifest.xml.
    • Убедитесь, что [project]/ios/fastlane/Appfile также соответствует идентификатору пакета Info.plist. Внесите в apple_id, itc_team_id, team_id данные вашей учетной записи.
  5. Настройте локальные учетные данные для входа в сторы.
    • Следуйте настройке Supply и убедитесь, что fastlane supply init успешно синхронизирует данные с вашей Play Store консоли. Относитесь к .json файлу как к своему паролю и не храните его в каких-либо общедоступных репозиториях систем контроля версий.
    • Ваш юзернейм ITunes Connect уже находится в ваших Appfileах в поле apple_id. Запишите в переменную среды FASTLANE_PASSWORD ваш пароль iTunes Connect. В противном случае он будет запрашиваться при загрузке в iTunes/TestFlight.
  6. Настройте цифровую подпись кода.
    • В Android есть два ключа для подписи: ключ развертывания и ключ загрузки. Конечные пользователи загружают .apk, подписанный ключом развертывания. Ключ загрузки используется для аутентификации .aab/.apk, загружаемого разработчиками в Play Store, и переподписывается ключом развертывания по завершению загрузки в Play Store.
      • Для ключа развертывания настоятельно рекомендуется использовать автоматическую подпись, которая управляется облаком. Для получения дополнительной информации см. официальную документацию Play Store.
      • Чтобы создать ключ загрузки следуйте инструкции по генерации ключа.
      • Настройте gradle на использование ключа загрузки во время создания билда приложения в режиме release, отредактировав android.buildTypes.release в [project]/android/app/build.gradle.
    • В iOS, когда вы будете готовы к тестированию и развертыванию с помощью TestFlight или App Store, создайте и подпишите приложение, используя сертификат распространения вместо сертификата разработки.

      • Создайте и загрузите сертификат распространения в консоли Apple Developer Account.
      • откройте [project]/ios/Runner.xcworkspace/ и выберите сертификат распространения на панели настроек вашей цели.
  7. Создайте скрипт Fastfile для каждой платформы.

    • Для Android следуйте руководству по развертыванию бета-версии Fastlane Android. Все редактирование может заключаться всего лишь в добавлении lane, которая вызывает upload_to_play_store. Установите значение аргумента aab в ../build/app/outputs/bundle/release/app-release.aab, чтобы использовать пакет, который flutter build уже подготовил.
    • Для iOS следуйте руководству по развертыванию бета-версии Fastlane iOS. Все редактирование может заключаться всего лишь в добавлении lane, которая вызывает build_ios_app с export_method: 'app-store' и upload_to_testflight. В iOS потребуется дополнительный билд, поскольку flutter build создает .app, а не архивирует .ipas для релиза.

Теперь вы готовы выполнять развертывание локально или переносить процесс развертывания в систему непрерывной интеграции (CI).

Выполнение развертывания локально


  1. Создайте приложение в режиме release.
    • flutter build appbundle.
    • flutter build ios --release --no-codesign. Сейчас не нужно подписывать, так как fastlane будет осуществлять подпись при архивировании.
  2. Запустите скрипт Fastfile на каждой платформе.
    • cd android, затем fastlane [название созданной вами lane].
    • cd ios, затем fastlane [название созданной вами lane].


Настройка сборки и развертывания в облаке


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

Главное, на что следует обратить внимание это то, что, поскольку облачные инстансы эфемерны и ненадежны, вы не должны оставлять свои учетные данные, такие как JSON учетной записи службы Play Store или сертификат распространения iTunes на сервере.

Системы непрерывной интеграции (CI), такие как Cirrus, обычно поддерживают зашифрованные переменные среды для хранения приватных данных.

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

  1. Сделайте учетные данные эфемерными.
    • В Android:

      • Удалите поле json_key_file из Appfile и сохраните содержимое JSON строки в зашифрованном переменной вашей CI системы. Используйте аргумент json_key_data в upload_to_play_store, чтобы прочитать переменную среды непосредственно в вашем Fastfile.
      • Сериализуйте свой ключ загрузки (например, используя base64) и сохраните его как зашифрованную переменную среды. Вы можете десериализовать его в своей системе CI на этапе установки с помощью
      • echo "$PLAY_STORE_UPLOAD_KEY" | base64 --decode > /home/cirrus/[directory # и имя файла, указанное в вашем gradle].keystore
        
    • В iOS:

      • Переместите локальную переменную среды FASTLANE_PASSWORD для использования зашифрованных переменных сред в системе CI.
      • Системе CI требуется доступ к вашему сертификату распространения. Рекомендуется использовать систему Fastlane Match для синхронизации ваших сертификатов на разных машинах.
  2. Рекомендуется использовать Gemfile вместо использования индетерминированного gem install fastlane в системе CI, чтобы гарантировать стабильность и воспроизводимость зависимостей fastlane между локальными и облачными машинами каждый раз. Однако этот шаг не является обязательным.

    • В обоих [project]/android и [project]/ios папках создайте Gemfile, содержащий следующее содержимое:
    • source "https://rubygems.org" gem "fastlane"
      
    • В обоих каталогах запустите bundle update и внесите Gemfile. и Gemfile.lock в систему контроля версий.
    • При локальном запуске используйте bundle exec fastlane вместо fastlane.
  3. Создайте тестовый скрипт CI, например .travis.yml или .cirrus.yml, в корне репозитория.

    • Сегментируйте свой скрипт для работы на платформах Linux и macOS.
    • Не забудьте указать зависимость от Xcode для macOS (например, osx_image: xcode9.2).
    • См. документацию Fastlane по CI для настройки конкретной CI.
    • На этапе установки, в зависимости от платформы, убедитесь, что:

      • Bundler доступен с помощью gem install bundler.
      • Для Android убедитесь, что Android SDK доступен и указан путь ANDROID_SDK_ROOT.
      • Запустите bundle install в [project]/android или [project]/ios.
      • Убедитесь, что Flutter SDK доступен и установлен в PATH.
    • На скриптовом этапе задачи CI:

      • Запустите приложение flutter build appbundle или flutter build ios --release --no-codesign, в зависимости от платформы.
      • cd android или cd ios
      • bundle exec fastlane [имя lane]

      Ссылки

      См. скрипт Cirrus для репозитория фреймворка Flutter.

      Другие службы


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


      А здесь пример проекта
Подробнее..

Navigation Component-дзюцу, vol. 3 Corner-кейсы

23.09.2020 10:08:04 | Автор: admin


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


Это третья и заключительная статья в цикле про различные кейсы навигации с Navigation Component-ом. Вы также можете ознакомиться с первой и второй частями



Если вы работаете с большим приложением, вероятно, вы уже разбили его на модули. Неважно, как именно. Может быть, вы создаёте отдельные модули для логики и UI, а может храните всю логику фичи (от взаимодействия с API до логики presentation-слоя) в одном модуле. Главное у вас могут быть кейсы, когда требуется осуществить навигацию между двумя независимыми модулями.


Где на схеме приложения кейсы с навигацией?


На картинке мы видим, что у нас есть как минимум два модуля: модуль :vacancy с одним экраном и модуль :company с двумя экранами вложенного flow. В рамках моего примера я построил навигацию из модуля :vacancy в модуль :company, которые не связаны друг с другом.


Существует три способа как это сделать, разберём их один за другим.


App-модуль + интерфейсы


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


Структура вашего приложения в этом способе


Структура приложения будет стандартной: есть app-модуль, который знает обо всех feature-модулях, есть feature-модули, которые не знают друг о друге. В этом способе ваши feature-модули пребывают в священном неведении о Navigation Component, и для навигации они будут определять интерфейсы примерно вот такого вида:


// ::vacancy moduleinterface VacancyRouterSource {    fun openNextVacancy(vacancyId: String)    // For navigation to another module    fun openCompanyFlow()}

А ваш app-модуль будет реализовывать эти интерфейсы, потому что он знает обо всех action-ах и навигации:


fun initVacancyDI(navController: NavController) {  VacancyDI.vacancyRouterSource = object : VacancyRouterSource {      override fun openNextVacancy(vacancyId: String) {          navController.navigate(              VacancyFragmentDirections                .actionVacancyFragmentToVacancyFragment(vacancyId = vacancyId)          )      }      override fun openCompanyFlow() {          initCompanyDI(navController)          navController.navigate(R.id.action__VacancyFragment__to__CompanyFlow)      }  }}

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


  • дополнительную работу в виде определения интерфейсов, реализаций, организации DI для проброса этих интерфейсов в ваши feature-модули;
  • отсутствие возможности использовать использовать Safe Args плагин, делегат navArgs, сгенерированные Directions, и другие фишки Navigation Component-а в feature-модулях, потому что эти модули ничего не знают про библиотеку.

Сомнительный, в общем, способ.


Графы навигации в feature-модулях + диплинки


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


Структура вашего приложения в этом способе


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


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


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/company_flow__nav_graph"    app:startDestination="@id/CompanyFragment">    <fragment        android:id="@+id/CompanyFragment"        android:name="company.CompanyFragment">        <deepLink app:uri="companyflow://company" />        <!-- Or with arguments -->        <argument android:name="company_id" app:argType="long" />        <deepLink app:uri="companyflow://company" />        <action            android:id="@+id/action__CompanyFragment__to__CompanyDetailsFragment"            app:destination="@id/CompanyDetailsFragment" />    </fragment>    <fragment        android:id="@+id/CompanyDetailsFragment"        android:name="company.CompanyDetailsFragment" /></navigation>

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


После этого мы можем использовать этот диплинк для открытия экрана CompanyFragment из модуля :vacancy :


// ::vacancy modulefragment_vacancy__button__open_company_flow.setOnClickListener {  // Navigation through deep link  val companyFlowUri = "companyflow://company".toUri()  findNavController().navigate(companyFlowUri)}

Плюс этого метода в том, что это самый простой способ навигации между двумя независимыми модулями. А минус что вы не сможете использовать Safe Args, или сложные типы аргументов (Enum, Serializable, Parcelable) при навигации между фичами.


P.S. Есть, конечно, вариант сериализовать ваши сложные структуры в JSON и передавать их в качестве String-аргументов в диплинк, но это как-то Странно.


Общий модуль со всем графом навигации


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


Структура вашего приложения в этом способе


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


В чём соль? Несмотря на то, что common-модуль не знает о реализациях ваших destination-ов (фрагментах, диалогах, activity), он всё равно способен объявить граф навигации в XML-файлах! Да, Android Studio начинает сходить с ума: все имена классов в XML-е горят красным, но, несмотря на это, все нужные классы генерируются, Safe Args плагин работает как нужно. И так как ваши feature-модули подключают к себе common-модуль, они могут свободно использовать все сгенерированные классы и пользоваться любыми action-ами вашего графа навигации.


Плюс этого способа наконец-то можно пользоваться всеми возможностями Navigation Component-а в любом feature-модуле. Из минусов:


  • добавился ещё один модуль в critical path каждого feature-модуля, которому потребовалась навигация;
  • отсутствует автоматический рефакторинг имён: если вы поменяете имя класса какого-нибудь destination-а, вам нужно будет не забыть, что надо поправить его в common-модуле.

Выводы по навигации в многомодульных приложениях


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

Работа с диплинками


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


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


Какую именно часть?


У меня было три кейса с диплинками, которые я хотел реализовать с помощью Navigation Component.


  • Открытие определённой вкладки нижней навигации допустим, я хочу через диплинк открыть вторую вкладку на главном экране после Splash-экрана

Посмотреть на картинке

Допустим, я хочу через диплинк открыть вкладку Favorites нижней навигации на главном экране после Splash-экрана:



  • Открытие определённого экрана ViewPager-а внутри конкретной вкладки нижней навигации

Посмотреть на картинке

Пусть я хочу открыть определённую вкладку ViewPager-а внутри вкладки Responses:



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

Посмотреть на картинке

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



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



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


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/app_nav_graph"    app:startDestination="@id/SplashFragment">    <fragment        android:id="@+id/SplashFragment"        android:name="ui.splash.SplashFragment" />    <fragment        android:id="@+id/MainFragment"        android:name="ui.main.MainFragment">        <deepLink app:uri="www.example.com/main" />    </fragment></navigation>

Затем я, следуя документации, добавил граф навигации с диплинком в Android Manifest:


<manifest xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    package="com.aaglobal.jnc_playground">    <application android:name=".App">        <activity android:name=".ui.root.RootActivity">            <nav-graph android:value="@navigation/app_nav_graph"/>            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>        </activity>    </application></manifest>

А потом решил проверить, работает ли то, что я настроил при помощи простой adb-команды:


adb shell am start \  -a android.intent.action.VIEW \  -d "https://www.example.com/main" com.aaglobal.jnc_playground

И-и-и нет. Ничего не завелось. Я получил краш приложения с уже знакомым исключением IllegalStateException: FragmentManager is already executing transactions. Дебаггер указывал на код, связанный с настройкой нижней навигации, поэтому я решил просто обернуть эту настройку в очередной Handler.post:


// MainFragment.kt  fragment with BottomNavigationViewoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {    super.onViewCreated(view, savedInstanceState)    if (savedInstanceState == null) {        safeSetupBottomNavigationBar()    }}private fun safeSetupBottomNavigationBar() {    Handler().post {        setupBottomNavigationBar()    }}

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


Это произошло, потому что в нашем случае путь диплинка был таким: мы запустили приложение, запустилась его единственная Activity. В вёрстке этой activity мы инициализировали первый граф навигации. В этом графе оказался элемент, который удовлетворял URI, мы отправили его через adb-команду вуаля, он сразу и открылся, проигнорировав указанный в графе startDestination.


Тогда я решил перенести диплинк в другой граф внутрь вкладки нижней навигации.


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/menu__search"    app:startDestination="@id/SearchContainerFragment">    <fragment        android:id="@+id/SearchContainerFragment"        android:name="tabs.search.SearchContainerFragment">        <deepLink app:uri="www.example.com/main" />        <action            android:id="@+id/action__SearchContainerFragment__to__CompanyFlow"            app:destination="@id/company_flow__nav_graph" />        <action            android:id="@+id/action__SearchContainerFragment__to__VacancyFragment"            app:destination="@id/vacancy_nav_graph" />    </fragment></navigation>

И, запустив приложение, я получил ЭТО:


Посмотреть на ЭТО


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


И что самое неприятное во всей этой истории это не баг, а фича.


Если почитать внимательно документацию про работу с диплинками в Navigation Component, можно найти следующий кусочек:


When a user opens your app via an explicit deep link, the task back stack is cleared and replaced with the deep link destination.

То есть наш back stack специально очищается, чтобы Navigation Component-у было удобнее работать с диплинками. Говорят, что когда-то давно, в бета-версии библиотеки всё работало адекватнее.


Мы можем это исправить. Корень проблемы в методе handleDeepLink NavController-а:


Кусочек handleDeepLink
public void handleDeepLink(@Nullable Intent intent) {    // ...    if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {        // Start with a cleared task starting at our root when we're on our own task        if (!mBackStack.isEmpty()) {            popBackStackInternal(mGraph.getId(), true);        }        int index = 0;        while (index < deepLink.length) {            int destinationId = deepLink[index++];            NavDestination node = findDestination(destinationId);            if (node == null) {                final String dest = NavDestination.getDisplayName(mContext, destinationId);                throw new IllegalStateException("Deep Linking failed:"                        + " destination " + dest                        + " cannot be found from the current destination "                        + getCurrentDestination());            }            navigate(node, bundle,                    new NavOptions.Builder().setEnterAnim(0).setExitAnim(0).build(), null);        }        return true;    }}

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


  • почти полностью скопировать к себе исходный код Navigation Component;
  • добавить свой собственный NavController с исправленной логикой (добавление исходного кода библиотеки необходимо, так как от NavController-а зависят практически все элементы библиотеки) назовём его FixedNavController;
  • заменить все использования исходного NavController-а на FixedNavController.

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


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


Покажи гифку


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


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



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


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


if (findMyNavController().isDeepLinkHandled && requireActivity().intent.data != null) {    val uriString = requireActivity().intent.data?.toString()    val selectedPosition = when {        uriString == null -> 0        uriString.endsWith("favorites") -> 0        uriString.endsWith("subscribes") -> 1        else -> 2    }    fragment_favorites_container__view_pager.setCurrentItem(selectedPosition, true)}

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



Navigation Component не поддерживает диплинки с условием из коробки. Если вы хотите поддержать такое поведение, Google предлагает действовать следующим образом:


  • через диплинк открыть экран, который требует авторизацию;
  • на этом экране проверить, авторизован ли пользователь, если нет открыть флоу авторизации поверх нужного экрана;
  • пройти auth flow, вернуть результат из вложенного графа и т.д., и т.п.

Возможности глобально решить мою задачу средствами Navigation Component-а я не нашёл.


Выводы по работе с диплинками в Navigation Component


  • Работать с ними больно, если требуется добавлять дополнительные действия или условия.
  • Объявлять диплинки ближе к месту их назначения классная идея, в разы удобнее AndroidManifest-а со списком поддерживаемых ссылок.

Бонус-секция кейсы БЕЗ проблем


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



Допустим, у вас есть экран вакансий, с которого вы можете перейти на другую вакансию.


Где на схеме приложения этот кейс?


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


<fragment  android:id="@+id/VacancyFragment"  android:name="com.aaglobal.jnc_playground.ui.vacancy.VacancyFragment"  android:label="Fragment vacancy"  tools:layout="@layout/fragment_vacancy">  <argument      android:name="vacancyId"      app:argType="string"      app:nullable="false" />  <action      android:id="@+id/action__VacancyFragment__to__VacancyFragment"      app:destination="@id/VacancyFragment" /></fragment>

И этого оказалось достаточно новый экран открывался поверх старого, при нажатии на кнопку Back навигация была корректной. Если бы я захотел, чтобы каждый новый экран открывался вместо текущего, было бы достаточно добавить атрибут popUpTo к моему action-у.



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


Где на схеме приложения этот кейс?


Я добавил контейнер для будущего фрагмента со списком в вёрстку вкладки нижней навигации:


<LinearLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <TextView        android:id="@+id/fragment_favorites_container__text__title"        style="@style/LargeTitle"        android:text="Favorites container" />    <androidx.fragment.app.FragmentContainerView        android:id="@+id/fragment_favorites_container__container__recommend_vacancies"        android:layout_width="match_parent"        android:layout_height="match_parent" /></LinearLayout>

А затем в runtime-е добавил нужный мне фрагмент в этот контейнер:


class FavoritesContainerFragment : Fragment(R.layout.fragment_favorites_container) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        childFragmentManager.attachFragmentInto(          containerId = R.id.fragment_container_view,          fragment = createVacancyListFragment()        )    }}

Метод attachFragmentInfo на childFragmentManager это extension-метод, который просто оборачивает всю работу с транзакциями, не более того.


А вот как я создал фрагмент:


class FavoritesContainerFragment : Fragment(R.layout.fragment_favorites_container) {    // ...    private fun createVacancyListFragment(): Fragment {        return VacancyListFragment.newInstance(          vacancyType = "favorites_container",          vacancyListRouterSource = object : VacancyListRouterSource {              override fun navigateToVacancyScreen(item: VacancyItem) {                  findNavController().navigate(                      R.id.action__FavoritesContainerFragment__to__VacancyFragment,                      VacancyFragmentArgs(vacancyId = "${item.name}|${item.id}").toBundle()                  )              }        }     }}

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



Пусть у меня есть несколько BottomSheetDialog-ов, между которыми я хочу перемещаться с помощью Navigation Component.


Где на схеме приложения этот кейс?


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


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/menu__favorites"    app:startDestination="@id/FavoritesContainerFragment">   <dialog        android:id="@+id/ABottomSheet"        android:name="ui.dialogs.dialog_a.ABottomSheetDialog">        <action            android:id="@+id/action__ABottomSheet__to__BBottomSheet"            app:destination="@id/BBottomSheet"            app:popUpTo="@id/ABottomSheet"            app:popUpToInclusive="true" />    </dialog>    <dialog        android:id="@+id/BBottomSheet"        android:name="ui.dialogs.dialog_b.BBottomSheetDialog">        <action            android:id="@+id/action__BBottomSheet__to__ABottomSheet"            app:destination="@id/ABottomSheet"            app:popUpTo="@id/BBottomSheet"            app:popUpToInclusive="true" />    </dialog></navigation>

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


Выводы по бонус-секции


Кейсы без проблем существуют.


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


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


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


Пример приложения на Github-е лежит здесь.


Полезные ссылки по теме


Подробнее..

Чем опасен postDelayed

24.09.2020 08:13:47 | Автор: admin

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


Проблема


Для начала рассмотрим как обычно используют postDelayed():


override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        view.postDelayed({            Log.d("test", "postDelayed")            // do action        }, 100)}

С виду всё хорошо, но давайте изучим этот код повнимательнее:


1) Это отложенное действие, выполнение которого мы будем ожидать через некоторое время. Зная насколько динамично пользователь может совершать переходы между экранами, данное действие должно быть отменено при смене фрагмента. Однако, этого здесь не происходит, и наше действие выполнится, даже если текущий фрагмент будет уничтожен.
Проверить это просто. Создаём два фрагмента, при переходе на второй запускаем postDelayed с большим временем, к примеру 5000 мс. Сразу возвращаемся назад. И через некоторое время видим в логах, что действие не отменено.


2) Второе "вытекает" из первого. Если в данном runnable мы передадим ссылку на property нашего фрагмента, будет происходить утечка памяти, поскольку ссылка на runnable будет жить дольше, чем сам фрагмент.


3) Третье и основное почему я об этом задумался:
Падения приложения, если мы обращаемся ко view после onDestroyView
synthitec java.lang.NullPointerException, поскольку кеш уже очищен при помощи _$_clearFindViewByIdCache, а findViewById отдаёт null
viewBinding java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null


Что же делать?


1 Если нам нужные размеры view использовать doOnLayout или doOnNextLayout


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


3 Использовать безопасный стиль.


Необходимо отписываться от нашего действия перед тем, как view будет отсоединено от window.


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)         Runnable {            // do action        }.let { runnable ->            view.postDelayed(runnable, 100)            view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {                override fun onViewAttachedToWindow(view: View) {}                override fun onViewDetachedFromWindow(view: View) {                    view.removeOnAttachStateChangeListener(this)                    view.removeCallbacks(runnable)                }            })        }    }

Обычный doOnDetach нельзя использовать, поскольку view может быть ещё не прикреплено к window, как к примеру в onViewCreated. И тогда наше действие будет сразу же отменено.


Где то во View.kt:


inline fun View.doOnDetach(crossinline action: (view: View) -> Unit) {    if (!ViewCompat.isAttachedToWindow(this)) { // выполнится это условие        action(this)  // и здесь мы сразу же отпишемся от действия    } else {        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {            override fun onViewAttachedToWindow(view: View) {}            override fun onViewDetachedFromWindow(view: View) {                removeOnAttachStateChangeListener(this)                action(view)            }        })    }}

Или же обобщим в extension:


fun View.postDelayedSafe(delayMillis: Long, block: () -> Unit) {        val runnable = Runnable { block() }        postDelayed(runnable, delayMillis)        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {            override fun onViewAttachedToWindow(view: View) {}            override fun onViewDetachedFromWindow(view: View) {                removeOnAttachStateChangeListener(this)                view.removeCallbacks(runnable)            }        })}

В принципе на этом можно остановится. Все проблемы решены. Но этим мы добавляем ещё один тип асинхронного выполнения к нашему проекту, что несколько усложняет его. Сейчас в мире Native Android есть 2 основных решения для асинхронного выполнения кода Rx и Coroutines.
Попробуем использовать их.
Сразу оговорюсь, что не претендую на 100% правильность по отношению к вашему проекту. В вашем проекте это может быть по другому/лучше/короче.


Coroutines


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


class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes), CoroutineScope by MainScope() {    override fun onDestroyView() {        super.onDestroyView()        coroutineContext[Job]?.cancelChildren()    }    override fun onDestroy() {        super.onDestroy()        cancel()    }}

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


В onDestroy уже закрываем scope, так как далее никаких задач не должно быть запущено.


Все подготовительные работы сделаны.
Перейдём к самой замене postDelayed:


fun BaseFragment.delayActionSafe(delayMillis: Long, action: () -> Unit): Job? {    view ?: return null    return launch {        delay(delayMillis)        action()    }}

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


RX


В RX за отмену подписок отвечает класс Disposable, но в RX нет Structured concurrency в отличии от coroutine. Из-за этого приходится прописывать это всё самому. Выглядит обычно это примерно так:


interface DisposableHolder {    fun dispose()    fun addDisposable(disposable: Disposable)}class DisposableHolderImpl : DisposableHolder {    private val compositeDisposable = CompositeDisposable()    override fun addDisposable(disposable: Disposable) {        compositeDisposable.add(disposable)    }    override fun dispose() {        compositeDisposable.clear()    }}

Также аналогично отменяем все задачи в базовом фрагменте:


class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes),    DisposableHolder by DisposableHolderImpl() {    override fun onDestroyView() {        super.onDestroyView()        dispose()    }    override fun onDestroy() {        super.onDestroy()        dispose()    }}

И сам extension:


fun BaseFragment.delayActionSafe(delayMillis: Long, block: () -> Unit): Disposable? {    view ?: return null    return Completable.timer(delayMillis, TimeUnit.MILLISECONDS).subscribe {        block()    }.also {        addDisposable(it)    }}

В заключении


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

Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru