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

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

Виды биометрии в мобильном приложении

22.11.2020 10:20:58 | Автор: admin
Для идентификации пользователя в приложении можно использовать биометрию например, сканеры радужной оболочки глаза, геометрии лица или отпечатка пальца. Хотя эти технологии известны и популярны, у начинающих разработчиков из-за недостатка информации до сих пор возникают те или иные вопросы.

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



Основные виды биометрии


Идентификация пользователей необходима во многих приложениях, которые обрабатывают личные данные, например, в онлайн-банках. Так, в России с 2018 года действует Единая биометрическая система (ЕБС), с помощью которой клиенты могут пользоваться услугами банков удаленно. По рекомендациям Банка России, в 2020 году все банки должны иметь возможность собирать биометрические данные пользователей.

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

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

  • Сканер отпечатка пальца (fingerprint) 57%
  • Сканер геометрии лица (face ID) 14%
  • Прочие методы: сканеры радужной оболочки глаза (IRIS) и геометрии руки (3-5%).

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

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

1) Сканер отпечатка пальца (fingerprint)


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

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



Freepik.com

2) Сканер геометрии лица (face ID)


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

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



Adobe Stock

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

3) Сканер радужной оболочки глаза (IRIS)


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

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

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



Adobe Stock

Как работает распознавание


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

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



Блок-схема с симметричными криптографическими ключами (источник)

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

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

Рассмотрим, как будет выглядеть процесс регистрации:

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

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


Блок-схема с асимметричными криптографическими ключами (источник)

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

Подводя итоги


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

Лучшие выпускные работы весеннего семестра Технопроектов

12.11.2020 14:14:33 | Автор: admin

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

Предыдущие публикации: 1, 2, 3, 4, 5.

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


Facepick, Технополис


Сервис поиска фотографий по лицам.

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

Система с помощью нейронных сетей сначала распознаёт лица на эталонных снимках, а затем кластеризует базу фотографий по найденным людям. Сервис умеет работать со внешними источниками: ВКонтактом, Одноклассниками, Яндекс.Диском и Google Drive.

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





Сервис представляет собой клиент-серверное приложение с REST API. Серверная часть состоит из двух основных компонентов: Java-приложения, в котором реализована логика взаимодействия пользователя с сервисом; и Python-приложения для определения лиц на фотографиях и извлечения их уникальных признаков при помощи нейронной сети.

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

Все сервисы разворачивается в отдельных Docker-контейнерах, а для их оркестрации используется docker-compose. Для реализации клиентской части приложения использовали TypeScript и React. В качестве персистентного хранилища данных применена база данных PostgreSQL.



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

Команда проекта: Вадим Дьячков, Егор Шахмин, Николай Рубцов.

Видео с защитой проекта.

Playmakers, Технопарк


Программно-аппаратное решение для журналирования спортивных тренировок.

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


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

В качестве аппаратной платформы выбрали модуль ESP32-WROOM в составе ESP32-devkit-v1. Она удовлетворяла определённым требованиям, под неё написаны утилиты генерирования кода и прошивки на Python, и к тому же её можно программировать из Arduino IDE как любую плату Arduino. На роль датчиков выбрали модуль Amperka IMU, в состав которого входит акселерометр и гироскоп. Вс` общение с датчиками происходит по протоколу I2C.

Схема прототипа:



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




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

Данные с акселерометра после фильтрации.


Данные с гироскопа после фильтрации.

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


Результат работы нейросети (оранжевая линия вероятность выполнения отжиманий, зеленая приседаний).

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

Интерфейс приложения:




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


Кроме того, авторы написали механизм валидации батчей после их сохранения в БД.

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

Для хранения данных о пользователях взяли Postgres, сам бэкенд приложения реализовали на Go с использованием фреймворка Gin.


Общая архитектура.

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

Команда проекта: Олег Соловьев, Темирлан Рахимгалиев, Владимир Елфимов, Антон Мартынов.

Видео с защитой проекта.

GestureApp, Техносфера


Фреймвор бесонтатного интерфейса.

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

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


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

Жесты распознаёт нейронная сеть MobileNet3D. Нейросеть выдает вероятности классов жестов, а также специального класса отсутствие жеста. Такая архитектура позволяет распознавать как статичные, так и динамические жесты. Обучалась нейросеть на датасете Jester. Достигнута точность прогнозирования F1=0,92.

Поток захвата получает кадры с фронтальной камеры и помещает их в конец списка. Если его длина больше 32, поток захвата будит поток исполнения модели. Тот берет 32 кадра из начала списка, прогнозирует классы, затем удаляет элементы с конца, пока не останется один элемент. Благодаря этому не нужна тяжелая синхронизация и существенно повышается производительность: 20 FPS на iPhone 11, 18 FPS на iPhone XS Max, 15 FPS на iPhone XR. А с помощью умного конвейера предварительной и последующей обработки энергопотребление снижено до минимума.

Пока что фреймворк работает только под iOS и Windows. В разработке применялись фреймворк PyTorch, платформа TwentyBN и язык Swift.

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

Команда проекта: Максим Матюшин, Борис Константиновский, Мирослав Морозов.

Видео с защитой проекта.



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

Магическая шаблонизация для Android-проектов

26.11.2020 10:08:34 | Автор: admin


Начиная с Android Studio 4.1, Google прекратил поддержку кастомных FreeMarker-ных шаблонов. Теперь вы не можете просто взять и написать свои ftl-файлы и сложить их в определённую папку, чтобы Android Studio самостоятельно добавила их в меню New Other. В качестве альтернативы нам предлагают разбираться в плагиностроении и создавать шаблоны изнутри плагинов IDEA. Нас в hh такая ситуация не очень устраивает, так как есть несколько полезных FreeMarker-ных шаблонов, которые мы постоянно используем и которые иногда нуждаются в обновлениях. Лезть в плагины, чтобы поправить какой-то шаблон? Нет уж, увольте.


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


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


*Geminio заклинание удвоения предметов во вселенной Гарри Поттера


Немного терминологии


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


Я буду называть шаблоном набор метаданных, который необходим в построении диалога для ввода пользовательских параметров. Рецептом назовём набор инструкций для исполнения, который отработает после того, как пользователь введёт данные. Когда я буду говорить про шаблонный текст генерируемого кода, я буду называть это ftl-шаблонами или FreeMarker-ными шаблонами.


Чем заменили FreeMarker?


Google уже давно объявил Kotlin предпочитаемым языком для разработки под Android. Все новые библиотеки, новые приложения в Google постепенно переписываются именно на Kotlin. И плагин android-а в Android Studio не стал исключением.


Как механизм шаблонов работал до Android Studio 4.1? Вы создавали папку для описания шаблона, заводили в нём несколько файлов globals.xml.ftl, template.xml, recipe.xml.ftl для описания параметров и инструкций выполнения шаблона, а ещё вы помещали туда ftl-шаблоны, служившие каркасом генерируемого кода. Затем все эти файлы перемещали в папку Android Studio/plugins/android/lib/templates/<category>. После запуска проекта Android Studio парсила содержимое папки /templates, добавляла в интерфейс меню New > дополнительные action-ы, а при вызове action-а читала содержимое template.xml, строила UI и так далее.


В целом понятно, почему в Google отказались от этого механизма. Создание нового шаблона на основе FreeMarker-ных recipe-ов раньше напоминало русскую рулетку: до запуска ты никогда не мог точно сказать, правильно ли его описал, все ли требуемые параметры заполнил. А потом, по реакции Android Studio, ты пытался определить, в какой конкретной букве ошибся. Находил ошибку, менял шаблон, и всё шло на новый круг. А число шаблонов растёт, растёт и количество мест в интерфейсе, куда хочется добавлять эти шаблоны. Раньше для добавления одного и того же шаблона в несколько мест интерфейса приходилось создавать дополнительные action-ы плагины. Нужно было упрощать.


Вот так и появился удобный Kotlin DSL для описания шаблонов. Сравните два подхода:


FreeMarker-ный подход

Вот так выглядел файл template.xml:


<?xml version="1.0"?><template    format="4"    revision="1"    name="HeadHunter BaseFragment"    description="Creates HeadHunter BaseFragment"    minApi="7"    minBuildApi="8">    <category value="HeadHunter" />    <!-- параметры фрагмента -->    <parameter        id="className"        name="Fragment Name"        type="string"        constraints="class|nonempty|unique"        default="BlankFragment"        help="The name of the fragment class to create" />    <parameter        id="fragmentName"        name="Fragment Layout Name"        type="string"        constraints="layout|nonempty|unique"        default="fragment_blank"        suggest="fragment_${classToResource(className)}"        help="The name of the layout to create" />    <parameter        id="includeFactory"        name="Include fragment factory method?"        type="boolean"        default="true"        help="Generate static fragment factory method for easy instantiation" />    <!-- доп параметры  -->    <parameter        id="includeModule"        name="Include Toothpick Module class?"        type="boolean"        default="true"        help="Generate fragment Toothpick Module for easy instantiation" />    <parameter        id="moduleName"        name="Fragment Toothpick Module"        type="string"        constraints="class|nonempty|unique"        default="BlankModule"        visibility="includeModule"        suggest="${underscoreToCamelCase(classToResource(className))}Module"        help="The name of the Fragment Toothpick Module to create" />    <thumbs>        <thumb>template_base_fragment.png</thumb>    </thumbs>    <globals file="globals.xml.ftl" />    <execute file="recipe.xml.ftl" /></template>

А ещё был файл recipe.xml.ftl:


<?xml version="1.0"?><recipe>    <#if useSupport>    <dependency mavenUrl="com.android.support:support-v4:19.+"/>    </#if>    <instantiate        from="res/layout/fragment_blank.xml.ftl"        to="${escapeXmlAttribute(resOut)}/layout/${escapeXmlAttribute(fragmentName)}.xml" />    <open file="${escapeXmlAttribute(resOut)}/layout/${escapeXmlAttribute(fragmentName)}.xml" />    <instantiate        from="src/app_package/BlankFragment.kt.ftl"        to="${srcOutRRR}/${className}.kt" />    <open file="${srcOutRRR}/${className}.kt" />    <#if includeModule>        <instantiate            from="src/app_package/BlankModule.kt.ftl"            to="${srcOutRRR}/di/${moduleName}.kt" />        <open file="${srcOutRRR}/di/${moduleName}.kt" />    </#if></recipe>

То же самое, но в Kotlin DSL

Сначала мы создаём описание шаблона с помощью специального TemplateBuilder-а:


val baseFragmentTemplate: Template    get() = template {        revision = 1        name = "HeadHunter BaseFragment"        description = "Creates HeadHunter BaseFragment"        minApi = 7        minBuildApi = 8        formFactor = FormFactor.Mobile        category = Category.Fragment        screens = listOf(            WizardUiContext.FragmentGallery,            WizardUiContext.MenuEntry        )        // параметры        val className = stringParameter {            name = "Fragment Name"            constraints = listOf(                Constraint.CLASS,                Constraint.NONEMPTY,                Constraint.UNIQUE            )            default = "BlankFragment"            help = "The name of the fragment class to create"        }        val fragmentName = stringParameter {            name = "Fragment Layout Name"            constraints = listOf(                Constraint.LAYOUT,                Constraint.NONEMPTY,                Constraint.UNIQUE            )            default = "fragment_blank"            suggest = { "fragment_${classToResource(className.value)}" }            help = "The name of the layout to create"        }        val includeFactory = booleanParameter {            name = "Include fragment factory method?"            default = true            help = "Generate static fragment factory method for easy instantiation"        }        // доп. параметры        val includeModule = booleanParameter {            name = "Include Toothpick Module class?"            default = true            help = "Generate fragment Toothpick Module for easy instantiation"        }        val moduleName = stringParameter {            name = "Fragment Toothpick Module"            constraints = listOf(                Constraint.CLASS,                Constraint.NONEMPTY,                Constraint.UNIQUE            )            visible = { includeModule.value }            suggest = { "${underscoreToCamelCase(classToResource(className.value))}Module" }            help = "The name of the Fragment Toothpick Module to create"            default = "BlankFragmentModule"        }        thumb { File("template_base_fragment.png") }        recipe = { templateData ->            baseFragmentRecipe(                moduleData = templateData as ModuleTemplateData,                className = className.value,                fragmentName = fragmentName.value,                includeFactory = includeFactory.value,                includeModule = includeModule.value,                moduleName = moduleName.value            )        }    }

Затем описываем рецепт в отдельной функции:


fun RecipeExecutor.baseFragmentRecipe(    moduleData: ModuleTemplateData,    className: String,    fragmentName: String,    includeFactory: Boolean,    includeModule: Boolean,    moduleName: String) {    val (projectData, srcOut, resOut, _) = moduleData    if (projectData.androidXSupport.not()) {        addDependency("com.android.support:support-v4:19.+")    }    save(getFragmentBlankLayoutText(), resOut.resolve("/layout/${fragmentName}.xml"))    open(resOut.resolve("/layout/${fragmentName}.xml"))    save(getFragmentBlankClassText(className, includeFactory), srcOut.resolve("${className}.kt"))    open(srcOut.resolve("${className}.kt"))    if (includeModule) {        save(getFragmentModuleClassText(moduleName), srcOut.resolve("/di/${moduleName}.kt"))        open(srcOut.resolve("/di/${moduleName}.kt"))    }}private fun getFragmentBlankClassText(className: String, includeFactory: Boolean): String {    return "..."}private fun getFragmentBlankLayoutText(): String {    return "..."}private fun getFragmentModuleClassText(moduleName: String): String {    return "..."}

Текст шаблонов перекочевал из FreeMarker-ных ftl-файлов в Kotlin-овские строчки.


По количеству кода получается примерно то же самое, но вот наличие подсказок IDE при описании шаблона помогает не ошибаться в значениях enum-ов и функциях. Добавьте к этому валидацию при создании объекта шаблона (например, покажется исключение, если вы забыли указать один из необходимых параметров), возможность вызова шаблона из разных меню в Android Studio и, кажется, у нас есть победитель.


Добавление шаблона через extension point


Чтобы новые шаблоны попали в существующие галереи новых объектов в Android Studio, нужно добавить созданный с помощью DSL шаблон в новую точку расширения (extension point) WizardTemplateProvider.


Для этого мы сначала создаём класс provider-а, наследуясь от абстрактного класса WizardTemplateProvider:


class MyWizardTemplateProvider : WizardTemplateProvider() {    override fun getTemplates(): List<Template> {        return listOf(            baseFragmentTemplate        )    }}

А затем добавляем созданный provider в качестве extension-а в plugin.xml файле:


<extensions defaultExtensionNs="com.android.tools.idea.wizard.template">    <wizardTemplateProvider implementation="ru.hh.plugins.geminio.actions.MyWizardTemplateProvider" /></extensions>

Запустив Android Studio, мы увидим шаблон baseFragmentTemplate в меню New->Fragment и в галерее нового фрагмента.


Покажи картинки!

Вот наш шаблон в меню New -> Fragments:



А вот он же в галерее нового фрагмента:



Если вы захотите самостоятельно пройти весь этот путь по добавлению нового шаблона из кода плагина, можете, во-первых, посмотреть на актуальный список готовых шаблонов в исходном коде Android Studio (который совсем недавно наконец-то добавили в cs.android.com), а во-вторых почитать вот эту статью на Medium (там хорошо описана последовательность действий по созданию нового шаблона, но показан не очень правильный хак с получением инстанса Project-а так лучше не делать).


А чем ещё можно заменить FreeMarker?


Кроме того, добавить шаблоны кода из плагинов можно с помощью File templates. Это очень просто: добавляете его в папку resources/fileTemplates и Вы восхитительны!


А можно поподробнее?

В папку /resources/fileTemplates вашего плагина нужно добавить шаблон нужного вам кода, например, /resources/fileTemplates/Toothpick Module.kt.ft .


package ${PACKAGE_NAME}.diimport toothpick.config.Moduleinternal class ${NAME}: Module() {    init {            // TODO    }}

Шаблоны кода работают на движке Velocity, поэтому можно добавлять в код шаблона условия и циклы. File template-ы имеют ряд встроенных параметров, например, PACKAGE_NAME (подставит package name, в зависимости от выбранного в Project View файла), MONTH (текущий месяц) и так далее. Каждый "неизвестный" параметр будет преобразован в поле ввода для пользователя.


После запуска Android Studio в меню New вы увидите новый пункт с названием вашего шаблона:



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



Примеры таких шаблонов вы можете подсмотреть в репозитории MviCore коллег из Badoo.


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


Что не так с новым механизмом


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


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


Механизм рендеринга шаблонов


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


Разобрались. Делимся.


Чтобы заставить Android Studio построить UI и сгенерировать код на основе нужного шаблона, придётся написать довольно много кода. Допустим, вы уже создалисобственный плагин, объявили зависимости от android-плагина, который лежит в Android Studio 4.1, добавили новый action, который будет отвечать за рендеринг. Тогда метод actionPerformed будет выглядеть вот так:


Обработка actionPerformed
override fun actionPerformed(e: AnActionEvent) {    val dataContext = e.dataContext    val module = LangDataKeys.MODULE.getData(dataContext)!!    var targetDirectory = CommonDataKeys.VIRTUAL_FILE.getData(dataContext)    if (targetDirectory != null && targetDirectory.isDirectory.not()) {       // If the user selected a simulated folder entry (eg "Manifests"), there will be no target directory        targetDirectory = targetDirectory.parent    }    targetDirectory!!    val facet = AndroidFacet.getInstance(module)    val moduleTemplates = facet.getModuleTemplates(targetDirectory)    assert(moduleTemplates.isNotEmpty())    val initialPackageSuggestion = facet.getPackageForPath(moduleTemplates, targetDirectory).orEmpty()    val renderModel = RenderTemplateModel.fromFacet(        facet,        initialPackageSuggestion,        moduleTemplates[0],        "MyActionCommandName",        ProjectSyncInvoker.DefaultProjectSyncInvoker(),        true,    ).apply {        newTemplate = template { ... } // build your template     }     val configureTemplateStep = ConfigureTemplateParametersStep(         model = renderModel,         title = "Template name",         templates = moduleTemplates     )     val wizard = ModelWizard.Builder()                    .addStep(configureTemplateStep).build().apply {          val resultListener = object : ModelWizard.WizardListener {          override fun onWizardFinished(result: ModelWizard.WizardResult) {              super.onWizardFinished(result)              if (result.isFinished) {                  // TODO do some stuff after creating files                  //   (renderTemplateModel.createdFiles)              }          }       }    }     val dialog = StudioWizardDialogBuilder(wizard, "Template wizard")            .setProject(e.project!!)            .build()     dialog.show()}

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


По логике программы, пользователь плагина нажимает Cmd + N на каком-то файле или package-е внутри какого-то модуля. Именно там мы и создадим пачку файлов, которые нам нужны. Поэтому необходимо определить, внутри какого же модуля и какой папки работаем.


Чтобы это сделать, воспользуемся возможностями AnActionEvent-а.


val dataContext = e.dataContextval module = LangDataKeys.MODULE.getData(dataContext)!!var targetDirectory = CommonDataKeys.VIRTUAL_FILE.getData(dataContext)if (targetDirectory != null && targetDirectory.isDirectory.not()) {    // If the user selected a simulated folder entry (eg "Manifests"), there will be no target directory    targetDirectory = targetDirectory.parent}targetDirectory!!

Как я уже рассказывал в своей статье с теорией плагиностроения, AnActionEvent представляет собой контекст исполнения вашего Action-а. Внутри этого класса есть свойство dataContext, из которого при помощи специальных ключей мы можем доставать нужные данные. Чтобы посмотреть, какие ещё ключи есть, обратите внимание на классы PlatformDataKeys, LangDataKeys и другие. Ключ LangDataKeys.MODULE возвращает нам текущий модуль, а CommonDataKeys.VIRTUAL_FILE выбранный пользователем в Project View файл. Немного преобразований и мы получаем директорию, внутрь которой нужно добавлять файлы.


val facet = AndroidFacet.getInstance(module)

Чтобы двигаться дальше, нам требуется объект AndroidFacet. Facet это, по сути, свойства модуля, которые специфичны для того или иного фреймворка. В данном случае мы получаем специфичное для Android описание нашего модуля. Из facet-а можно достать, например, package name, указанный в AndroidManifest.xml вашего android-модуля.


val moduleTemplates = facet.getModuleTemplates(targetDirectory)assert(moduleTemplates.isNotEmpty())val initialPackageSuggestion = facet.getPackageForPath(moduleTemplates, targetDirectory).orEmpty()

Из facet-а мы достаём объект NamedModuleTemplate контейнер для основных путей android-модуля: путь до папки с исходным кодом, папки с ресурсами, тестами и т.д. Благодаря этому объекту можно найти и package name для подстановки в будущие шаблоны кода.


val renderModel = RenderTemplateModel.fromFacet(    facet,    initialPackageSuggestion,    moduleTemplates[0],    "MyActionCommandName",    ProjectSyncInvoker.DefaultProjectSyncInvoker(),    true,).apply {    newTemplate = template { ... } // build your template}

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


  • AndroidFacet модуля, в котором мы создаем файлы;
  • первый предлагаемый пользователю package name (его можно будет использовать в параметрах шаблона);
  • объект, хранящий пути к основным папкам модуля, NamedModuleTemplate;
  • строковую константу для идентификации WriteCommandAction (внутренний объект IDEA, предназначенный для операций модификации кода) она нужна для того, чтобы у вас сработал Undo;
  • объект, отвечающий за синхронизацию проекта после создания файлов, ProjectSyncInvoker;
  • и, наконец, флаг true или false, который отвечает за то, можно ли открывать все созданные файлы в редакторе кода или нет.

val configureTemplateStep = ConfigureTemplateParametersStep(    model = renderModel,    title = "Template name",    templates = moduleTemplates)val wizard = ModelWizard.Builder()    .addStep(configureTemplateStep)    .build().apply {        val resultListener = object : ModelWizard.WizardListener {               override fun onWizardFinished(result: ModelWizard.WizardResult) {                       super.onWizardFinished(result)                       if (result.isFinished) {                               // TODO do some stuff after creating files                   //   (renderTemplateModel.createdFiles)                       }               }     }}val dialog = StudioWizardDialogBuilder(wizard, "Template wizard")            .setProject(e.project!!)            .build()dialog.show()

Финал!


Для начала создаем ConfigureTemplateParametersStep, который прочитает переданный объект template-а и сформирует UI страницы wizard-диалога, потом пробрасываем step в модель Wizard-диалога и наконец-то показываем сам диалог.


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


Самое сложное позади! Мы показали диалог, который взял на себя работу по построению UI из модели шаблона и обработку рецепта внутри шаблона.


Остаётся только откуда-то получить сам шаблон. И рецепт.


Откуда взять модель шаблона


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


Мне показалось, что самый простой формат это yaml-конфиг. Почему именно yaml? Потому что: а) выглядит проще XML, и б) внутри IDEA уже есть подключенная библиотечка для его парсинга SnakeYaml, позволяющая в одну строчку прочитать весь файл в Map<String, Any>, который можно дальше крутить как угодно.


В данный момент конфиг шаблона выглядит так:


yaml-конфиг шаблона
requiredParams:  name: HeadHunter BaseFragment  description: Creates HeadHunter BaseFragmentoptionalParams:  revision: 1  category: fragment  formFactor: mobile  constraints:    - kotlin  screens:    - fragment_gallery    - menu_entry  minApi: 7  minBuildApi: 8widgets:  - stringParameter:      id: className      name: Fragment Name      help: The name of the fragment class to create      constraints:        - class        - nonempty        - unique      default: BlankFragment  - stringParameter:      id: fragmentName      name: Fragment Layout Name      help: The name of the layout to create      constraints:        - layout        - nonempty        - unique      default: fragment_blank      suggest: fragment_${className.classToResource()}  - booleanParameter:      id: includeFactory      name: Include fragment factory method?      help: Generate static fragment factory method for easy instantiation      default: true  - booleanParameter:      id: includeModule      name: Include Toothpick Module class?      help: Generate fragment Toothpick Module for easy instantiation      default: true  - stringParameter:      id: moduleName      name: Fragment Toothpick Module      help: The name of the Fragment Toothpick Module to create      constraints:        - class        - nonempty        - unique      default: BlankModule      visibility: ${includeModule}      suggest: ${className.classToResource().underlinesToCamelCase()}Modulerecipe:  - instantiateAndOpen:      from: root/src/app_package/BlankFragment.kt.ftl      to: ${srcOut}/${className}.kt  - instantiateAndOpen:      from: root/res/layout/fragment_blank.xml.ftl      to: ${resOut}/layout/${fragmentName}.xml  - predicate:      validIf: ${includeModule}      commands:        - instantiateAndOpen:            from: root/src/app_package/BlankModule.kt.ftl            to: ${srcOut}/di/${moduleName}.kt

Вся конфигурация шаблона делится на 4 секции:


  • requiredParams параметры, обязательные для каждого шаблона;
  • optionalParams параметры, которые можно спокойно опустить при описании шаблона. В данный момент эти параметры ни на что не влияют, потому что мы не подключаем созданный на основе конфига шаблон через extension point.
  • widgets набор параметров шаблона, которые зависят от пользовательского ввода. Каждый из этих параметров в конечном итоге превратится в виджет на UI диалога (textField-ы, checkbox-ы и т.п.);
  • recipe набор инструкций, которые выполняются после того, как пользователь заполнит все параметры шаблона.

Написанный мною плагин парсит этот конфиг, конвертирует его в объект шаблона Android Studio и пробрасывает в RenderTemplateModel.


В самой конвертации практически не было ничего интересного кроме парсинга выражений. Я имею в виду строчки вот такого вида:


suggest: ${className.classToResource().underlinesToCamelCase()}Module

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


sealed class Command {    data class Fixed(        val value: String    ) : Command()    data class Dynamic(        val parameterId: String,        val modifiers: List<GeminioRecipeExpressionModifier>    ) : Command()    data class SrcOut(        val modifiers: List<GeminioRecipeExpressionModifier>    ) : Command()    data class ResOut(        val modifiers: List<GeminioRecipeExpressionModifier>    ) : Command()    object ReturnTrue : Command()    object ReturnFalse : Command()}

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


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


recipe:  # Можно писать вот так  - instantiate:      from: root/src/app_package/BlankFragment.kt.ftl      to: ${srcOut}/${className}.kt  - open:      file: ${srcOut}/${className}.kt  # А можно одной командой:  - instantiateAndOpen:      from: root/src/app_package/BlankFragment.kt.ftl      to: ${srcOut}/${className}.kt

Какие ещё есть плюсы в Geminio


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


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


Roadmap


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


  • нет поддержки enum-параметров, которые бы отображались на UI в виде combobox-ов;
  • не все команды из FreeMarker-ных шаблонов поддерживаются в рецептах например, нет автоматического добавления зависимостей в build.gradle, merge-а XML-ресурсов;
  • новые шаблоны страдают от той же проблемы, что и FreeMarker-ные шаблоны нет адекватной валидации, которая бы точно сказала, где именно случилась ошибка;
  • и нет никаких подсказок IDE при описании шаблона.

Заключение


Заканчивать нужно на позитивной ноте. Поэтому вот немного позитива:


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

Всем успешной автоматизации.


Полезные ссылки


Подробнее..

SPM модуляризация проекта для увеличения скорости сборки

11.11.2020 12:15:25 | Автор: admin
Привет, Хабр! Меня зовут Эрик Басаргин, я iOS-разработчик в Surf.

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

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



Почему именно SPM


Ответ прост это нативно и ново. Он не создает overhead в виде xcworkspace, как Cocoapods, к примеру. К тому же SPM open-source проект, который активно развивается. Apple и сообщество исправляют в нем баги, устраняют уязвимости, обновляют вслед за Swift.

Делает ли это сборку проекта быстрее


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

Note: Эффективность модуляризации напрямую зависит от правильного разбиения проекта на модули.

Как сделать эффективное разбиение


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

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

  • CommonAssets набор ваших Assets'ов и public интерфейс для доступа к ним. Обычно он генерируется с помощью SwiftGen.
  • CommonExtensions набор расширений, к примеру Foundation, UIKit, дополнительные зависимости.

Разделять flow'ы приложения. Рассмотрим древовидную структуру, где MainFlow главное flow приложения. Представим, что у нас новостное приложение.

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

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

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

Когда нужно выносить компонент в отдельный модуль


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

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

Создаём проект с использованием SPM


Рассмотрим создание тривиального тестового проекта. Я использую Multiplatform App project на SwiftUI. Платформа и интерфейс тут не имеют значения.

Note: Чтобы быстро создать Multiplatform App, нужен XCode 12.2 beta.

Создаём проект и видим следующее:



Теперь создадим первый модуль Common:

  • добавляем папку Frameworks без создания директории;
  • создаём SPM-пакет Common.



  • Добавляем поддерживаемые платформы в файл Package.swift. У нас это platforms: [.iOS(.v14), .macOS(.v10_15)]



  • Теперь добавляем наш модуль в каждый таргет. У нас это SPMExampleProject для iOS и SPMExampleProject для macOS.



Note: Достаточно подключать к таргетам только корневые модули. Они не добавлены как подмодули.

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

Как подключить зависимость у локального SPM-пакета


Добавим пакет AdditionalInfo как Common, но без добавления к таргетам. Теперь изменим Package.swift у Common пакета.



Добавлять больше ничего не нужно. Можно использовать.

Пример, приближенный к реальности


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

  1. Создаем новый корневой модуль по инструкции выше.
  2. Добавляем к нему корневые каталоги Scripts и Templates.
  3. Добавляем в корень модуля файл Palette.xcassets и пропишем какие-либо color set'ы.
  4. Добавляем пустой файл Palette.swift в Sources/Palette.
  5. Добавим в папку Templates шаблон palette.stencil.
  6. Теперь нужно прописать конфигурационный файл для SwiftGen. Для этого добавим файл swiftgen.yml в папку Scripts и пропишем в нем следующее:

xcassets:  inputs:    - ${SRCROOT}/Palette/Sources/Palette/Palette.xcassets  outputs:    - templatePath: ${SRCROOT}/Palette/Templates/palette.stencil      params:        bundle: .module        publicAccess: true      output: ${SRCROOT}/Palette/Sources/Palette/Palette.swift


Итоговый внешний вид модуля Palette

Модуль Palette мы с вами настроили. Теперь надо настроить запуск SwiftGen, чтобы палитра генерировалась при старте сборки. Для этого заходим в конфигурацию каждого таргета и создаем новую Build Phase назовём ее Palette generator. Не забудьте перенести эту Build Phase на максимально высокую позицию.

Теперь прописываем вызов для SwiftGen:

cd ${SRCROOT}/Palette/usr/bin/xcrun --sdk macosx swift run -c release swiftgen config run --config ./Scripts/swiftgen.yml

Note: /usr/bin/xcrun --sdk macosx очень важный префикс. Без него при сборке вылетит ошибка: unable to load standard library for target 'x86_64-apple-macosx10.15.


Пример вызова для SwiftGen

Готово доступ к цветам можно получить следующим образом:
Palette.myGreen (Color type in SwiftUI) и PaletteCore.myGreen (UIColor/NSColor).

Подводные камни


Перечислю то, с чем мы успели столкнуться.

  • Всплывают ошибки архитектуры и портят всю логику разбиения на модули.
  • SwiftLint & SwiftGen не уживаются вместе при подгрузке их через SPM. Причина в разных версиях yml.
  • В крупных проектах не получится сразу избавиться от Cocoapods. А разбивать уже созданный проект с закреплёнными версиями подов настоящее испытание, потому что SPM только развивается и не везде поддерживается. Но SPM и Cocoapods более-менее работают параллельно: разве что поды могут кидать ошибку MergeSwiftModule failed with a nonzero exit code. Это происходит довольно редко, а решается очисткой и пересборкой проекта.
  • На данный момент SPM не позволяет прописать пути поиска библиотек. Приходится явно указывать их с завязкой на -L$(BUILD_DIR).

SPM замена Bundler?


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

SPM дает возможность вызывать swift run, если добавить Package.swift в корень вашего проекта. Что это нам дает? К примеру, можно вызвать fastlane или swiftlint. Пример вызова:

swift run swiftlint --autocorrect.
Подробнее..

Переходим В OFFLINE FIRST с использованием Core Data и Managed Document(s)

19.11.2020 12:06:35 | Автор: admin
Придя в компанию Мегафон как iOS-разработчик, Валентин Чернов попал в сегодняшний основной тренд переход приложений в оффлайн. Основная работа Валентина разработка главного приложения Мегафона, мобильного личного кабинета. Это в нем вы можете смотреть баланс и вообще управлять им, подключать/отключать услуги и сервисы, смотреть сторисы, участвовать в конкурсах и видеть персональные предложения от партнеров Мегафона.

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

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



Задача


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

Стек технологий


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

Swift + Objective-C


Большая часть кода (80% нашего проекта) написан на Objective-C. А уже новый код мы пишем на Swift.

Модульная архитектура


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

Submodules (библиотеки)


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

Core Data для локального хранения информации


При выборе для нас главным критерием были нативность и интеграция с iOS фреймворками. А эти преимущества Core Data стали решающими при его выборе:

  • Автосохранение стека и данных, которые получаем;
  • Удобная работа с моделью данных, достаточно удобная работа с графическим редактором для составления сущностей (и возможности их править, передавать новым разработчикам и т.д.)
  • Поддержка миграции и версионирования;
  • Ленивая загрузка объектов;
  • Работа в многопоточном режиме;
  • Отслеживание изменений;
  • Интеграция с UI (FRC);
  • Работа с запросами в БД на более высоком уровне (NSPredicates).

UIManaged document


У UI kit есть встроенный класс, называемый UIManagedDocument, и который является подклассом UIDocument. Его основное отличие при инициализации управляемого документа указывается URL-адрес для расположения документа в локальном или удаленном хранилище. Затем объект документа полностью создает стек Core Data прямо из коробки, который используется для доступа к постоянному хранилищу документа с использованием объектной модели (.xcdatamodeld) из основного пакета приложения. Это удобно и имеет смысл, даже несмотря на то, что мы живем уже в 21 веке:

  • UIDocument автосохраняет текущее состояние сам, с определенной частотой. Для особо критичных секций мы можем вручную вызывать сохранение.
  • Можно отслеживать состояния документа. Если документ открыт для работы или находится в каких-то конфликтных ситуациях например, мы осуществляем сохранение из разных точек, и где-то вдруг мы вызвали конфликт, мы можем это отследить, обработать, поправить и уведомить пользователя правильной понятной ошибкой.
  • UIDocument позволяет читать и записывать документ асинхронно.
  • Он может создать стек Core data из коробки.
  • Есть встроенная функция хранения в iCloud и синхронизации с облаком. Это как раз то, к чему мы в будущем стремимся.
  • Поддержка версионности.
  • Используется Document based app парадигма представление модели данных как контейнер для хранения этих данных. Если посмотреть на классическую модель MVC в документации Apple, можем увидеть, что Core data создана как раз для того, чтобы управлять этой моделью и помогать нам на более высоком уровне абстракции работать с данными. На уровне модели работаем, подключая UIManagedDocument со всем созданным стеком. А сам документ рассматриваем как контейнер, который хранит Core data и все данные из кэша (от экранов, пользователей). Плюс это могут быть картинки, видео, тексты любая информация.
  • Мы же рассматриваем наше приложение, его запуск, авторизацию пользователей и все его данные как некий большой документ (файл), в котором хранится история нашего пользователя. И которую мы можем передавать, очищать и владеть ею на уровне приложения:



Процесс


Как мы проектировали архитектуру


Процесс проектирования у нас проходит в несколько этапов:

  1. Анализ технического задания.
  2. Отрисовка диаграммы UML-диаграмм. Мы используем в основном три типа UML-диаграмм: class diagram (диаграмма классов), flow chart (блок-схема), sequence diagram (диаграмма последовательностей). Это прямая обязанность senior-разрабочиков, но могут делать и разработчики с меньшим опытом. Это даже приветствуется, так как позволяет хорошо погрузиться в задачу и изучить все ее тонкости. Что помогает найти в ТЗ какие-то недоработки, а также структурировать всю информацию по задаче. И мы стараемся учитывать кросс-платформенность нашего приложения мы тесно работаем с Android-командой, рисуя одну схему на две платформы и стараясь использовать основные общепринятые паттерны проектирования от банды четырёх.
  3. Ревью архитектуры. Как правило, ревью и оценку проводит коллега из смежной команды.
  4. Реализация и тестирование на примере одного UI модуля.
  5. Масштабирование. Если тестирование проходит успешно, мы масштабируем архитектуру на все приложение.
  6. Рефакторинг. Чтобы проверить, не упустили ли мы что-нибудь.

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

Что было


Нашей отправной точкой была стандартная MVC архитектура это связанные между собой слои:

  • UI слой, полностью программно сверстанный с использованием Objective C;
  • Класс презентации (модель);
  • Сервисный слой, где мы работаем с сетью.

Activity indicator был расположен в том месте схемы, где мы держим пользователя в режиме ожидания он хочет быстрого результата, но вынужден смотреть на какие-то лоадеры, индикаторы и прочие сигналы. Это было нашим слабым местом в user experience:



Переходный период


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

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

В правой части схемы мы разбили получение данных на команды паттерн Command нацелен выполнить какую-то базовую команду и получить результат. В случае iOS мы используем наследников NSOperation:



Каждая команда, которую вы здесь видите это операция, в которой есть логическая единица ожидаемого действия. Это получение данных из БД (или сети) и сохранение этих данных в Core data. Или, например, главная задача AcquireCommand это не только вернуть фасаду источник данных, но и дать нам возможность разрабатывать код таким образом, чтобы получать данные через фасад. То есть взаимодействие с операциями идет через данный фасад.

А основная задача операций передать данные DataSource для DataSourceFacade. Конечно, мы выстраиваем логику так, чтобы как можно быстрее показать данные пользователю. Как правило, внутри DataSourceFacade у нас есть операционная очередь, где мы запускаем наши NSOperations. В зависимости от настроенных условий мы можем принять решение, когда показывать данные из кэша, а когда получать из сети. При первом запросе источника данных в фасаде мы идем в БД Core data, достаем оттуда через FetchCommand данные (если они там есть) и моментально возвращаем их пользователю.

Одновременно запускаем параллельный запрос данных через сеть, и когда этот запрос выполняется, то результат приходит в базу данных, сохраняется в ней, и после мы получаем update нашего DataSource. Этот update попадает уже в UI. Так мы минимизируем время ожидания данных, а пользователь, получая их мгновенно, не замечает разницы. А обновленные данные он получит сразу, как база данных получит ответ от сети.

Как стало


К такой более лаконичной схеме мы идем (и придем в итоге):



Сейчас из этого у нас есть:

  • UI слой,
  • фасад, через который мы предоставляем наш DataSource,
  • команда, которая этот DataSource вместе с updates возвращает.

Что такое DataSource и почему мы о нем так много говорим


DataSource это объект, который предоставляет данные для слоя презентации и соответствует заранее определенному протоколу. А протокол должен быть подстроен под наш UI и предоставлять данные для нашего UI (неважно, для конкретного экрана или для группы экранов).

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

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

Мы у себя используем несколько вариантов DataSource, потому что у нас много Objective C legacy кода то есть, мы не везде можем легко наш Swiftовый DataSource воткнуть. Еще мы пока не везде используем коллекции, но в будущем перепишем код именно для использования CollectionView экранов.

Пример одного из наших DataSource:



Это DataSource для коллекции (он так и называется CollectionDataSource) и это достаточно несложный класс с точки зрения интерфейса. Он принимает в себя коллекцию, настроенный fetchedResultsController и CellDequeueBlock. Где CellDequeueBlock type alias, в котором мы описываем стратегию по созданию ячеек.

То есть мы создали DataSource и присвоили его коллекции, вызвав у fetchedResultsController performFetch, и дальше вся магия возложена на взаимодействие нашего класса DataSource, fetchedResultsController и возможность у делегата получать обновления из базы данных:



FetchedResultsController сердце нашего DataSource. В документации Apple вы найдете много информации по работе с ним. Как правило, мы получаем все данные с его помощью и новые данные, и данные, которые были обновлены или удалены. При этом мы параллельно запрашиваем данные из сети. Как только данные были получены и сохранились в БД, мы получили update у DataSource, и update пришел к нам в UI. То есть одним запросом мы и получаем данные, и показываем их в разных местах классно, удобно, нативно!

И везде, где можно использовать уже готовые DataSource с таблицами или с коллекциями, мы это делаем:



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

Как только мы будем готовы перевести экран полностью в Swift-реализацию, достаточно будет убрать Objective C-обертку и можно работать напрямую со Swiftовым протоколом, благодаря кастомному DataSource.

Сейчас мы используем три основных варианта DataSources:

  • TableViewDatasource + cell strategy (стратегия по созданию ячеек);
    CollectionViewDatasource + cell strategy (вариант с коллекциями);
    CustomDataSource кастомный вариант, его мы сейчас используем больше всего.

    Результаты


    После всех шагов по проектированию, реализации и взаимодействию с legacy кодом бизнес получил:

    • Существенно повысилась скорость доставки данных до пользователя за счет кэширования это, наверное, очевидный и логичный результат.
    • Мы теперь на шаг ближе к парадигме offline first.
    • Настроили процессы архитектурного кроссплатформенного ревью внутри iOS & Android команд все причастные к этому проекту разработчики владеют информацией и легко обмениваются опытом между командами.
    • Как бонус получили хорошую документацию к проекту за счет схем и описаний. Мы ее показываем нашим новым разработчикам, чтобы им было проще понять, как у нас проброшен мостик между legacy и новым кодом, и как работает сам процесс кэширования.
    • Мы уложились в сжатые спортивные сроки, и это на живом проекте. У нас получилось, условно говоря, провести ремонт, никого не выселяя из офиса, но все продолжали работать, и даже не дышали строительной пылью.

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

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

    Ссылки


    • Document-based programming guide. Это довольно старый документ, Apple его уже не рекомендует использовать. Но я бы порекомендовал посмотреть хотя бы для дополнительного развития. Там очень много полезной информации.
    • Document-based WWDC: первый и второй
    • DataSources
Подробнее..

Flutter. Слушатель клавиатуры без платформенного кода

19.11.2020 14:19:12 | Автор: admin
Всем привет! Меня зовут Дмитрий Андриянов, я Flutter-разработчик в Surf.

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



Эта статья будет вам полезна, если вы:

  • Пишете на Flutter и хотите узнать, что находится у него под капотом.
  • Интересуетесь, как MediaQuery предоставляет данные о UI.
  • Хотите реализовывать интересные штуки на Flutter, покопавшись в нём на более глубоком уровне.

Зачем нам понадобилось написать слушатель без натива


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

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

Исследуем MediaQuery и копаем вглубь


Из MediaQuery мы можем получить данные о размерах системных UI-элементов, которые перекрывают дисплей:

// Поле с данными элементов перекрывающих дисплейMediaQuery.of(context).viewInsets// Отвечает за данные клавиатурыMediaQuery.of(context).viewInsets.bottom

Пример:

class KeyboardScreen extends StatefulWidget { @override _KeyboardScreenState createState() => _KeyboardScreenState();}class _KeyboardScreenState extends State<KeyboardScreen> { @override Widget build(BuildContext context) {   return Scaffold(     body: Column(       mainAxisAlignment: MainAxisAlignment.center,       children: [         Text('Keyboard: ${MediaQuery.of(context).viewInsets.bottom}'),         const SizedBox(height: 20),         TextField(),       ],     ),   ); }}

image

Первая мысль использовать MediaQuery.of(context).viewInsets при изменениях значения: 0 клавиатура скрыта, иначе видна. Но в момент обращения к MediaQueryData мы получим значение, а не Stream, который нужно слушать.

У этого решения две проблемы:
  1. Для использования требуется контекст, что накладывает дополнительные ограничения. Например когда у вас есть модель данных связанная с UI, реагирующая на появление клавиатуры.
  2. viewInsets не дает возможности подписаться на изменения значения.

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

Переходим в исходный код метода MediaQueryData of и видим:

static MediaQueryData of(BuildContext context, { bool nullOk = false }) { assert(context != null); assert(nullOk != null); final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>(); if (query != null)   return query.data; if (nullOk)   return null; throw FlutterError.fromParts(<DiagnosticsNode>[   ErrorSummary('MediaQuery.of() called with a context that does not contain a MediaQuery.'),   ErrorDescription(   ),   context.describeElement('The context used was') ]);}

final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>();

В этой строке по дереву родителей ищется класс MediaQuery. У полученного виджета берутся и возвращаются данные в виде экземпляра MediaQueryData.

Смотрим в MediaQuery: оказывается, это наследник InheritedWidget, и он создаётся в разных виджетах:

image

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

Например, файл dialog:

MediaQuery( data: MediaQuery.of(context).copyWith(   // iOS does not shrink dialog content below a 1.0 scale factor   textScaleFactor: math.max(textScaleFactor, 1.0), ),

Самый верхний MediaQuery создаётся в файле widgets/app.dart.
Класс _MediaQueryFromWindow:

class _MediaQueryFromWindow extends StatefulWidget { const _MediaQueryFromWindow({Key key, this.child}) : super(key: key); final Widget child; @override _MediaQueryFromWindowsState createState() => _MediaQueryFromWindowsState();}class _MediaQueryFromWindowsState extends State<_MediaQueryFromWindow> with WidgetsBindingObserver { @override void initState() {   super.initState();   WidgetsBinding.instance.addObserver(this); }// ACCESSIBILITY@overridevoid didChangeAccessibilityFeatures() { setState(() {   // The properties of window have changed. We use them in our build   // function, so we need setState(), but we don't cache anything locally. });}// METRICS@overridevoid didChangeMetrics() { setState(() {   // The properties of window have changed. We use them in our build   // function, so we need setState(), but we don't cache anything locally. });}@overridevoid didChangeTextScaleFactor() { setState(() {   // The textScaleFactor property of window has changed. We reference   // window in our build function, so we need to call setState(), but   // we don't need to cache anything locally. });}// RENDERING@overridevoid didChangePlatformBrightness() { setState(() {   // The platformBrightness property of window has changed. We reference   // window in our build function, so we need to call setState(), but   // we don't need to cache anything locally. });} @override Widget build(BuildContext context) {   MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);   if (!kReleaseMode) {     data = data.copyWith(platformBrightness: debugBrightnessOverride);   }   return MediaQuery(     data: data,     child: widget.child,   ); } @override void dispose() {   WidgetsBinding.instance.removeObserver(this);   super.dispose(); }}

Что здесь происходит:


1. Класс _MediaQueryFromWindowsState замешивает миксин WidgetsBindingObserver, чтобы использоваться в качестве наблюдателя за изменениями системного UI из Flutter.

2. В initState вызываем WidgetsBinding.instance.addObserver(this); addObserver принимает на вход экземпляр наблюдателя. В данном случае this, так как текущий класс замешивает WidgetsBindingObserver.

3. WidgetsBindingObserver предоставляет методы, которые вызываются при изменении соответствующих метрик:
didChangeAccessibilityFeatures вызывается при изменении набора активных на данный момент специальных возможностей в системе.
didChangeMetrics вызывается при изменении размеров приложения из-за системы. Например, при повороте телефона или влиянии системного UI (появлении клавиатуры).
didChangeTextScaleFactor вызывается при изменении коэффициента масштабирования текста на платформе.
didChangePlatformBrightness вызывается при изменении яркости.

4. Самое главное, что объединяет эти методы, в каждом из них вызывается setState. Это запускает метод build, заново строит объект MediaQueryData

Widget build(BuildContext context) { MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);

и передает его вниз по дереву до места вызова MediaQuery.of(context).ИмяПоля:

Подробнее про биндинг можно прочесть в статье моего коллеги Миши Зотьева.

Вывод: мы можем получать изменения системного UI, используя WidgetsBinding и WidgetsBindingObserver.

Реализация слушателя клавиатуры


Начнём реализовывать слушатель клавиатуры на основе этих данных. Для начала создадим класс:

class KeyboardListener with WidgetsBindingObserver {}

Добавим геттер bool чтобы знать, видна ли клавиатура.

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

double get currentKeyboardHeight => _currentKeyboardHeight;double _currentKeyboardHeight = 0;bool get _isVisibleKeyboard => _currentKeyboardHeight > 0;Future(() { final double newKeyboardHeight =     WidgetsBinding.instance.window.viewInsets.bottom; if (newKeyboardHeight > _currentKeyboardHeight) {   /// Новая высота больше предыдущей  клавиатура открылась   _onShow();   _onChange(true); } else if (newKeyboardHeight < _currentKeyboardHeight) {   /// Новая высота меньше предыдущей  клавиатура закрылась   _onHide();   _onChange(false); } _currentKeyboardHeight = newKeyboardHeight;});

Мы знаем, что при видимой клавиатуре в viewInsets.bottom значение больше 0, при скрытой 0.

bool get _isVisibleKeyboard => _currentKeyboardHeight > 0; выполняет проверку: если высота клавиатуры больше нуля, то она видна.

Но на некоторых устройствах с Android 9 при закрытии клавиатуры высота не всегда становилась 0. Открытая клавиатура могла передать значение 400, а закрытая 150. А в следующий раз она передавала уже 0. Нестабильный и сложно уловимый баг.

Поэтому я решил отказаться от возможности получать размер клавиатуры из экземпляра слушателя и стал проверять:

WidgetsBinding.instance.window.viewInsets.bottom > 0;

Это решило проблему.

Теперь реализуем непосредственно прослушивание изменений для вызова колбэков:

@overridevoid didChangeMetrics() { _listener();}void _listener() { if (isVisibleKeyboard) {   _onChange(true); } else {   _onChange(false); }}void _onChange(bool isOpen) { /// Тут вызываются внешние слушатели}

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

Как использовать


class _KeyboardScreenState extends State<KeyboardScreen> { bool _isShowKeyboard = false; KeyboardListener _keyboardListener = KeyboardListener(); @override void initState() {   super.initState();   _keyboardListener.addListener(onChange: (bool isVisible) {     setState(() {       _isShowKeyboard = isVisible;     });   }); } @override void dispose() {   _keyboardListener.dispose();   super.dispose(); } @override Widget build(BuildContext context) {   return Scaffold(     body: Column(       mainAxisAlignment: MainAxisAlignment.center,       children: [         Text('Keyboard: $_isShowKeyboard'),         const SizedBox(height: 20),         TextField(),       ],     ),   ); }}

image

Полный код


import 'dart:math';import 'dart:ui';import 'package:flutter/widgets.dart';typedef KeyboardChangeListener = Function(bool isVisible);class KeyboardListener with WidgetsBindingObserver { static final Random _random = Random(); /// Колбэки, вызывающиеся при появлении и сокрытии клавиатуры final Map<String, KeyboardChangeListener> _changeListeners = {}; /// Колбэки, вызывающиеся при появлении клавиатуры final Map<String, VoidCallback> _showListeners = {}; /// Колбэки, вызывающиеся при сокрытии клавиатуры final Map<String, VoidCallback> _hideListeners = {}; bool get isVisibleKeyboard =>     WidgetsBinding.instance.window.viewInsets.bottom > 0; KeyboardListener() {   _init(); } void dispose() {   // Удаляем текущий класс из списка наблюдателей   WidgetsBinding.instance.removeObserver(this);    // Очищаем списки колбэков   _changeListeners.clear();   _showListeners.clear();   _hideListeners.clear(); } /// При изменениях системного UI вызываем слушателей @override void didChangeMetrics() {   _listener(); } /// Метод добавления слушателей String addListener({   String id,   KeyboardChangeListener onChange,   VoidCallback onShow,   VoidCallback onHide, }) {   assert(onChange != null || onShow != null || onHide != null);   /// Для более удобного доступа к слушателям используются идентификаторы   id ??= _generateId();   if (onChange != null) _changeListeners[id] = onChange;   if (onShow != null) _showListeners[id] = onShow;   if (onHide != null) _hideListeners[id] = onHide;   return id; } /// Методы удаления слушателей void removeChangeListener(KeyboardChangeListener listener) {   _removeListener(_changeListeners, listener); } void removeShowListener(VoidCallback listener) {   _removeListener(_showListeners, listener); } void removeHideListener(VoidCallback listener) {   _removeListener(_hideListeners, listener); } void removeAtChangeListener(String id) {   _removeAtListener(_changeListeners, id); } void removeAtShowListener(String id) {   _removeAtListener(_changeListeners, id); } void removeAtHideListener(String id) {   _removeAtListener(_changeListeners, id); } void _removeAtListener(Map<String, Function> listeners, String id) {   listeners.remove(id); } void _removeListener(Map<String, Function> listeners, Function listener) {   listeners.removeWhere((key, value) => value == listener); } String _generateId() {   return _random.nextDouble().toString(); } void _init() {   WidgetsBinding.instance.addObserver(this); // Регистрируем наблюдателя } void _listener() {   if (isVisibleKeyboard) {     _onShow();     _onChange(true);   } else {     _onHide();     _onChange(false);   } } void _onChange(bool isOpen) {   for (KeyboardChangeListener listener in _changeListeners.values) {     listener(isOpen);   } } void _onShow() {   for (VoidCallback listener in _showListeners.values) {     listener();   } } void _onHide() {   for (VoidCallback listener in _hideListeners.values) {     listener();   } }}

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

Итог


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

Это решение находится в SurfGear, пакет keyboard_listener.
Подробнее..

Как добавить 3D в iOS приложение c помощью SceneKit

26.11.2020 12:17:06 | Автор: admin


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


Предыстория


Изначально в Кошельке 3D-рендер был написан, как кроссплатформенное решение, и использовал под капотом OpenGL. Данная реализация по-прежнему используется в Android-версии приложения.


Здесь стоит отметить, что в 3D был не только экран карты, но и список карт. Так как в перспективе мы планируем совместить список карт с возможностью показа предложений из каталога, решено было уйти от этого решения. Теперь для списка карт мы используем UICollectionView c кастомным layoutом. Кстати, коллеги из Android-команды в свое время выступали на конференции с докладом по этой теме: Приложение Кошелёк: Как мы оживляем карты.



В 2018 году на WWDC Apple объявила, что в следующих версиях iOS будет прекращена поддержка OpenGL, и рекомендовала всем переходить на Metal. Детальное сравнение двух технологий можно посмотреть в докладе Metal for OpenGL Developers с WWDC. К слову, несмотря на то, что OpenGL уже несколько лет, как deprecated, в iOS 14 он по-прежнему доступен для использования; понятно, что обновлений для него с тех пор нет. Предварительно оценив время и силы, которые необходимо потратить на перенос существующего решения, мы решили использовать новую технологию и попробовать написать MVP. Самым простым решением было использовать какой-нибудь готовый движок для работы с 3D, где будет поддержка как Metal, так и OpenGL. Это бы дало возможности настройки параметров через визуальный редактор, а также упростило работу, связанную с моделями, текстурами, камерой, освещением и анимациями. Но тянуть движок в проект ради прямоугольника с красивой текстурой это слишком. Если взять для сравнения Unity, то это и увеличение размера приложения (судя по документации, минимальный размер приложения составляет 12 МБ, для нас это равно +20% к размеру приложения), и стоимость 200$ в год, и необходимость показывать лого на сплэш-скрине (решается покупкой более дорогой лицензии), и увеличенное время загрузки. А если смотреть в сторону open source решений (LibGDX, urho3d), то поддержки Metal у них нет. И тут нам на помощь приходит нативное решение от Apple SceneKit, лишенное недостатков, описанных выше (отсутствует влияние на размер приложения если сравнивать пустые проекты, то импортирование SceneKit увеличило размер приложения на 1 КБ; бесплатность, визуальный редактор и прочее).


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


SceneKit


SceneKit высокоуровневый фреймворк, работающий поверх Metal или OpenGL, упрощающий работу с 3D-графикой и позволяющий легко добавлять анимации, работу с физикой, частицы и реалистичный рендеринг, основанный на физике. Самым интересным для нас является как раз последний пункт.
SceneKit поддерживает несколько концепций для визуализации материалов это blinn, constant, lambert, phong и physically based. Подробнее про каждую из моделей можно почитать в документации. Но сравнение этих подходов хорошо видно на изображении:


Как видите, physically based тут явный фаворит.


Physically based rendering (PBR) концепция визуализации материалов, основанная на физических принципах. Подробно данная тема раскрыта в докладе Advances in SceneKit Rendering с WWDC. Если кратко, то суть данного подхода в том, что любая поверхность состоит из трёх составляющих: диффузной, металлической и неровности. Попробуем разобраться с каждой из составляющих на примере создания золотой поверхности:


  • диффузная (diffuse) определяет количество света, диффузно отраженного от поверхности. Рассеянный свет одинаково отражается во всех направлениях и поэтому не зависит от точки зрения.
    Так как золото отражает однородный свет по всей поверхности, то наша диффузная текстура будет полностью жёлтой и выглядеть так:
  • металлическая (metalness) определяет, насколько поверхность схожа с металлом. Более низкие значения (тёмные цвета) приводят к тому, что материал больше похож на диэлектрическую поверхность. Более высокие значения (яркие цвета) делают поверхность более металлической, и, соответственно, более зеркальной.
    Так как мы рисуем полностью золотую текстуру без примесей, а золото это металл, то и metalness текстура должна быть полностью белой.
  • неровность (roughness) определяет, насколько поверхность кажется гладкой. Более низкие значения (тёмные цвета) приводят к тому, что материал выглядит блестящим, с четко определёнными зеркальными бликами. Более высокие значения (яркие цвета) заставляют зеркальные блики расширяться, а диффузное свойство материала становится более световозвращающим.
    Золото практически идеально ровный материал, поэтому текстура неровности приближена к чёрному, а для создания дефектов на поверхности добавим более светлые участки:

Сложив вместе три составляющих, мы получим:


Для наглядности связь между metalness и roughness представлена ниже:


Еще один важный момент: данная технология доступна в SceneKit с iOS 10, но на практике выяснилось, что из-за перехода на вторую версию Metal итоговое изображение очень сильно отличается, и в этом случае нам приходится поддерживать две конфигурации. На тот момент минимальной поддерживаемой версией в приложении была iOS 9. А так как даже 3.8% пользователей на версиях с 9 по 10 по количеству для нас равнялись населению небольшого города (55 000), отказаться от них было тяжелым решением. Взвесив все за и против (в первую очередь это упрощение верстки, так как с iOS 11 началась эра экранов iPhone X и safe area, а также снижение нагрузки на тестирование), мы решили поднять минимальную версию до iOS 11.


Реализация


Основой SceneKit является сцена (SCNScene), поэтому первым делом необходимо добавить её. В документации сказано, что сцену и изображения текстур для оптимизации лучше сохранять в каталог с разрешением .scnassets. Мы можем добавить его через File > New > File > раздел Resource > SceneKit Catalog. В дальнейшем все файлы, связанные со SceneKit, будем помещать в него. Для создания сцены у нас есть два простых варианта: либо добавить её программно и из кода добавлять на неё объекты, либо же воспользоваться визуальным редактором, создав сцену через File > New > File > раздел Resource > SceneKit Scene File. Выберем второй вариант, в результате получим сцену с камерой.

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


Следующим шагом будет добавление 3D-модели на сцену. Можно воспользоваться готовыми примитивами:


Но вряд ли это то, что вам нужно. Можно, конечно, попробовать составить объект из примитивов, но текстурирование объектов SceneKit не поддерживает, нет возможности редактировать текстурные координаты, доступен только просмотр. Поэтому предусмотрена поддержка сторонних моделей, выгруженных из 3D-редакторов. Мы использовали Blender. Основным форматом для загрузки моделей в SceneKit является DAE (Digital Asset Exchange или Collada). При загрузке мы можем сразу же конвертировать его в SCN формат, или сделать это позже через Editor > Convert to SceneKit file format (.scn). Также доступна конвертация и в обратную сторону.


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


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


Добавляем текстуру в проект, а дальше в настройке сцены в разделе Background and Lighting используем в качестве Environment. При необходимости можно её же использовать в качестве фона, поместив в Background. Стоит отметить, что текстуры могут быть в HDR, а размеры текстуры должны соответствовать определенному формату. Для сферической текстуры (мы находимся в сфере), как у нас, соотношение ширины к высоте должно быть 2 к 1, для кубической (мы находимся в кубе) соотношение сторон должно быть 6 к 1. Детально про форматы можно почитать в документации, раздел Using Cube Map Textures.


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


Осталось добавить материал. Как было сказано выше, для PBR нам понадобится три варианта текстур: diffuse, metalness, roughness.


Так как мы хотели добиться металлического эффекта отражения на части карты, как от фольги, при отрисовке metalness текстуры эти участки делаем светлыми, чтобы приблизить поверхность к металлу. Основной же фон остается чёрным, так как основная поверхность карты это пластик. Для roughness текстуры, наоборот, основную поверхность делаем светлой, чтобы придать эффект матовости при отражении, а выделенные участки затемняем, чтобы получить зеркальное отражение.
Переходим на вкладку настройки материала в Shading, выбираем Physically Based и устанавливаем текстуры. Одним из основных параметров у текстур является intensity. Свойство изменяется в диапазоне от 0.0 до 1.0 и в зависимости от типа текстуры приводит к разным результатам. Например, при уменьшении значения для текстуры нормалей (к этому вернёмся ниже) мы получаем сглаживание поверхности, для metalness и roughness текстур общее затемнение. Таким образом, в случае необходимости мы можем сгладить общее влияние текстуры на конечный результат.


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


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


Теперь нужно вывести сцену на экран. Для этого используется объект SCNView, наследник UIView. Можем инициализировать его из кода, указав нашу сцену, либо просто добавив его на view у UIViewController через storyboard. Для дебага мы можем выставить свойство allowsCameraControl в true, чтобы иметь возможность управлять камерой сцены, и showStatistic для отображения FPS и количества полигонов на экране.


let sceneView = SCNView(frame: view.bounds, options: nil)sceneView.scene = SCNScene(named: "SceneKitAsset.scnassets/Scene.scn")sceneView.allowsCameraControl = truesceneView.showsStatistics = trueview.addSubview(sceneView)

Собираем проект и получаем итоговый результат:


Apple Watch


Отдельный плюс использования SceneKit это поддержка Apple Watch. Для отображения карты на часах необходимо проделать всего несколько шагов. Добавим нашу сцену и все материалы, связанные с ней, в Target extension Apple Watch.
Для вывода сцены вместо SCNView используется WKInterfaceSCNScene. Добавляем его в Storyboard на наш контроллер и создаем свойство:


@IBOutlet weak var sceneInterface: WKInterfaceSCNScene!

Работа с WKInterfaceSCNScene выглядит почти так же, как и SCNView, но за исключением пары моментов. Не поддерживается автоматическое управление камерой, поэтому для оживления нашей сцены добавляем бесконечную анимацию вращения карты. Также не поддерживаются текстуры окружения в HDR формате, и в этот раз для установки источников света воспользуемся свойством autoenablesDefaultLighting, с помощью которого на сцену автоматически будет добавлен всенаправленный источник света.


guard let scene = SCNScene(named: "SceneKitAsset.scnassets/Scene.scn") else {    return}// Take card and start forever rotation animationif let card = scene.rootNode.childNode(withName: "Card", recursively: true) {    card.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0,                                                              y: 2,                                                              z: 0,                                                              duration: 1)))}

В результате получим:


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


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


func grayscaleImage(image: UIImage) -> UIImage? {    guard let ciImage = CIImage(image: image) else {        return nil    }    let grayscale = ciImage.applyingFilter("CIPhotoEffectNoir", parameters: [:])    let black = grayscale.applyingFilter("CIToneCurve", parameters: [        "inputPoint0": CIVector(x: 0.0, y: 0.0),        "inputPoint4": CIVector(x: 1.0, y: 0.9)    ])    return UIImage(ciImage: black)}

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


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


func noise() -> UIImage? {    guard let randomGenerator = CIFilter(name: "CIPhotoEffectMono") else {        return nil    }    randomGenerator.setValue(CIFilter(name: "CIRandomGenerator")?.outputImage,                             forKey: kCIInputImageKey)    randomGenerator.setDefaults()    if let ciImage = randomGenerator.outputImage {        let rect = CGRect(x: Int.random(in: 0...100),                          y: 0,                          width: 1_024,                          height: 1_024)        if let ref = CIContext(options: nil).createCGImage(ciImage, from: rect) {            let image = UIImage(cgImage: ref)            return image        }    }    return nil}

Помимо прочего, у нас была необходимость изменять направление источников света в зависимости от положения телефона. Здесь вроде бы всё просто: посчитали угол наклона устройства через CMMotionManager и повернули источники света. Но текстура окружения не поддерживает поворот, поэтому пришлось и камеру, и карту положить в отдельный объект, и уже его вращать. За счет этого создаётся впечатление смены направления источников света.
На самом деле в продакшне использование текстуры окружения принесло нам больше всего проблем. На версиях ниже iOS 12 сцена отказывалась рендериться с включённой текстурой окружения, а на части устройств (от iPhone 6 до iPhone X) при этом чёткой зависимости не прослеживалось. С выходом iOS 14 мы получили очень долгую загрузку, поэтому на этих версиях программно добавляем источники света. Результат уже, конечно, выглядит не так интересно.
Чтобы не фризить приложение, объекты можно загрузить в бэкграунде с помощью метода
prepareObjects:withCompletionHandler:, но здесь есть один важный момент. Несмотря на вызов completionHandler, так как ещё ни одного кадра не было отрисовано, на мгновение можно увидеть пустую сцену. Чтобы этого избежать, необходимо дождаться отрисовки в делегате SCNView SCNSceneRenderDelegate в методе renderer(_:didRenderScene:atTime:)


Итоги


Как это часто бывает, Apple предоставила разработчикам довольно качественный инструмент для работы с 3D графикой. В результате, приложив минимум усилий, мы получили MVP, на основе которого можно строить продакшн решение.


Плюсы подхода:


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

Минусы подхода:


  • отсутствует кроссплатформенность;
  • сложно что-то оптимизировать, использование Metal дало бы более широкие возможности в этом плане.

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


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

Подробнее..

Конечные автоматы на страже порядка

26.11.2020 12:17:06 | Автор: admin


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

Суть задачи


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

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

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



Естественно, мы заподозрили, что решение получится очень громоздким. Например, с тарифами задача подразумевала 7 полноценных экранов, массу различных диалогов и уведомлений. От сервера необходимо было сразу получать определенные данные. К этому добавилась и обработка различных состояний доступности редактирования выбранных значений; предвыбранные значения, которые нам приходят с сервера; возможность выбирать значения только на увеличение (речь о возможности запланировать тариф с большими значениями относительно текущих настроек тарифа). И многое другое. С пакетами была похожая картина, но меньше масштабом.

К тому же было еще два небольших условия от бизнеса:

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

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

Выбор решения


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

if (hasTariff) {if (hasErrorTariff) {           // Ошибка оплаты тарифа} else if (isProcessedTariff) {           // тариф ожидает оплаты} else {           //тариф активен}} else {//нет тарифа}

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

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

enum class State {PROCESS, ERROR, ACTIVE}when (state) {   PROCESS -> // тариф ожидает оплаты   ERROR -> // Ошибка оплаты тарифа   ACTIVE -> //тариф активен}

Третий вариант: найти что-то более описываемое, понятное и масштабируемое. Конечно же, это конечные автоматы (машины состояний).

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


Конечные автоматы


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


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

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

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

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

Тогда мы обратили внимание на библиотеку Tinder. Достоинств у неё оказалось больше, чем недостатков, что и сыграло позднее в её пользу. Она написана на Kotlin, у неё удобная DSL, библиотеку регулярно обновляют. А её главный недостаток трудно дорабатывается. Но всё же мы остановились на Tinder.

Библиотека Tinder


Код библиотеки:

val stateMachine = StateMachine.create<State, Event, SideEffect> {    initialState(State.Solid)    state<State.Solid> {        on<Event.OnMelted> {            transitionTo(State.Liquid, SideEffect.LogMelted)        }    }    state<State.Liquid> {        on<Event.OnFroze> {            transitionTo(State.Solid, SideEffect.LogFrozen)        }        on<Event.OnVaporized> {            transitionTo(State.Gas, SideEffect.LogVaporized)        }    }    state<State.Gas> {        on<Event.OnCondensed> {            transitionTo(State.Liquid, SideEffect.LogCondensed)        }    }    onTransition {        val validTransition = it as? StateMachine.Transition.Valid ?: return@onTransition        when (validTransition.sideEffect) {            SideEffect.LogMelted -> logger.log(ON_MELTED_MESSAGE)            SideEffect.LogFrozen -> logger.log(ON_FROZEN_MESSAGE)            SideEffect.LogVaporized -> logger.log(ON_VAPORIZED_MESSAGE)            SideEffect.LogCondensed -> logger.log(ON_CONDENSED_MESSAGE)        }    }}

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

Состояния переключаются просто: передаём в Transition объекта stateMachine событие, которое хотим отправить. В stateMachine есть описание всех возможных состояний. А внутри них мы можем описать те события, которые могут произойти.

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

Реализация


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

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

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


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

class TariffFlowStateMachine constructor(       val selectedStateMachine: TariffSelectedStateMachine,       val presetStateMachine: TariffPresetStateMachine,       val packageStateMachine: TariffPackageStateMachine,       val tariffStateMachine: TariffStateMachine,       val paymentStateMachine: TariffPaymentStateMachine) {   private val initialState = State.Init      val state: State       get() = when (stateMachine.state) {           is State.RootsState.RootSelectedState -> selectedStateMachine.state           is State.RootsState.RootPresetState -> presetStateMachine.state           is State.RootsState.RootPackageState -> packageStateMachine.state           is State.RootsState.RootTariffState -> tariffStateMachine.state           is State.RootsState.RootPaymentState -> paymentStateMachine.state           else -> State.Init   }

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

Например, так выглядит автомат выбора данных:


Автомат сборки пакета:


Автомат сборки тарифа:


А это автомат оплаты:


Приятный бонус


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

stateMachine = flowStateMachine.stateMachinestateFlowable = flowStateMachine.stateMachine.state//region utilityprivate fun assertTransition(initial: State, event: Event, expected: State) {  //given  val stateMachine = givenStateIs(initial)  val stateSubscriber = stateFlowable.test()  //when  stateMachine.transition(event)  //assert  stateSubscriber.assertLast(expected)}private fun givenStateIs(state: State): StateMachine<State, Event, SideEffect> {  return stateMachine.with { initialState(state) }}private fun TestSubscriber<State>.assertLast(expected: State) {  this.assertValueAt(this.valueCount() - 1, expected)}@Testfun `given state PaidPromotion on Error should result in PaymentMethods`() {  assertTransition(        initial = State.PaidPromotion(paymentMethod = PaymentMethod.CARD),        event = Event.Error(),        expected = State.PaymentMethods(navigateBack = true, reload = false)  )}

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

В заключение


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

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

Surf на DartUP 2020

24.11.2020 16:19:42 | Автор: admin
4 5 декабря пройдёт единственная в России конференция о Flutter и Dart на русском языке DartUp 2020. Это главное Flutter-событие года. Вас ждут доклады известных российских и мировых спикеров, которые расскажут об экосистеме Dart и дадут практические советы и лайфхаки, а также партнёрские мероприятия.

Мы в Surf тоже участвуем в DartUp в качестве партнёров и подготовили несколько движух. Участвуйте в них, чтобы прокачать себя и сделать мир Flutter-разработки лучше вот так, без регистрации (почти) и смс.

Что будет? Рассказываем.



Вас ждёт:

  • Код-ревью ваших репозиториев в прямом эфире. Его проведёт Артём Зайцев, руководитель Flutter-отдела в Surf.
  • Возможность поучаствовать в развитии Flutter-комьюнити и сделать мир Flutter-разработки ещё круче: предлагайте любые идеи, как улучшить работу с фреймворком.
  • Доклады от Flutter Teamlead Surf Жени Сатурова и ведущего Flutter-разработчика Миши Зотьева.
  • Много подарков и много общения.

А теперь подробнее

Сбор идей для Open Source


Мы в Surf участвуем в развитии Flutter-сообщества и приглашаем вас присоединиться к этому. Как?

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

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

Среди всех участников рандомно разыграем мерч Surf.

Код-ревью ваших репозиториев в прямом эфире


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

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

Спикеры



Gear up, ускоряем Flutter-разработку!


Евгений Сатуров, Flutter TeamLead Surf, активный спикер и технический евангелист в Surf
Два года назад Flutter был всего лишь хайповой технологией с неясными перспективами. Ставки сделаны, и теперь имеется несколько крупных проектов, а также большой репозиторий с опенсорс наработками SurfGear. Главная миссия SurfGear ускорять разработку и упрощать реализацию типовых задач. Доклад представляет собой обзор решений, которые помогут оптимизировать процессы вашего Flutter-проекта.



Flutter под капотом


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


Участие и регистрация


Когда: 4 и 5 декабря
Где: онлайн

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

Программа и регистрация на официальном сайте мероприятия
Подробнее..

Материальный дизайн. Создание анимаций в Kivy

15.11.2020 22:16:27 | Автор: admin


Приветствую всех любителей и знатоков языка программирования Python!
В этой статье я покажу, как работать с анимациями в кроссплатформенном фреймворке Kivy в связке с библиотекой компонентов Google Material Design KivyMD. Мы рассмотрим структуру Kivy проекта, использование material компонентов для создания тестового мобильного приложения с одним экраном и большим количеством анимаций. Статья будет большая с большим количеством GIF анимаций поэтому наливайте кофе и погнали!

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



Итак, для работы нам понадобится фреймворк Kivy:

pip install kivy

И библиотека KivyMD, которая предоставляет виджеты в стиле Material Design для фреймворка Kivy:

pip install https://github.com/kivymd/KivyMD/archive/master.zip

Все готово к работе! Откроем PyCharm и создадим новый проект CallScreen со следующей структурой катологов:



Структура может любая. Ни фреймворк Kivy, ни библиотека KivyMD не требует никаких обязательных директорий, кроме стандартного требования в корне проекта должен быть файл с именем main.py. Это точка входа в приложение:



В каталоге data/images я разместил графические ресурсы, которые требуются приложению:

В директории uix/screens/baseclass у нас будет размещаться файл callscreen.py с одноименным Python классом, в котором мы будем реализовывать логику работы экрана приложения:



А в директории uix/screens/kv мы создадим файл callscreen.kv (пока оставим пустым) с описанием UI на специальном DSL языке Kivy Language:



Когда проект создан, мы можем открыть файл callscreen.py и реализовать класс экрана нашего тестового приложения.

callscreen.py:

import osfrom kivy.lang import Builderfrom kivymd.uix.screen import MDScreen# Читаем и загружаем KV файлwith open(os.path.join(os.getcwd(), "uix", "screens", "kv", "callscreen.kv"), encoding="utf-8") as KV:    Builder.load_string(KV.read())class CallScreen(MDScreen):    pass


Класс CallScreen унаследован от виджета MDScreen библиотеки KivyMD (почти все компоненты этой библиотеки имеют префикс MD Material Design). MDScreen это аналог виджета Screen фреймворка Kivy из модуля kivy.uix.screenmanager, но с дополнительными свойствами. Также MDScreen позволяет размещать в себе виджеты и контроллы один над другим следующим образом:


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

В точке входа в приложение файл main.py создадим класс TestCallScreen, унаследованный от класса MDApp с обязательным методом build, который должен возвращать виджет или лайоут, для отображения его на экране. В нашем случае это будет созданный ранее класс экрана CallScreen.

main.py:

from kivymd.app import MDAppfrom uix.screens.baseclass.callscreen import CallScreenclass TestCallScreen(MDApp):    def build(self):        return CallScreen()TestCallScreen().run()


Это уже готовое приложение, которое отображает пустой экран. Если запустить файл main.py, увидим:



Теперь приступим к разметке UI экрана в файле callscreen.kv. Для этого нужно создать одноименное с базовым классом правило, в котором мы будем описывать виджеты и их свойства. Например, если у нас есть Python класс c именем CallScreen, то и правило в KV файле должно иметь точно такое же имя. Хотя вы можете создавать все элементы интерфейса прямо в коде, но это, мягко говоря, не правильно. Сравните:

MyRootWidget:    BoxLayout:        Button:        Button:

И аналог на Python:

root = MyRootWidget()box = BoxLayout()box.add_widget(Button())box.add_widget(Button())root.add_widget(box)

Совершенно очевидно, что дерево виджетов намного читабельнее в Kv Language, чем в Python коде. К тому же, когда появятся аргументы у виджетов, ваш Python код станет просто сплошной кашей и уже через день вы не сможете разобраться в нем. Поэтому кто бы что ни говорил, но если фреймворк позволяет описывать элементы UI посредством декларативного языка, это плюс. Ну, а в Kivy это двойной плюс, потому что в Kv Language еще можно выполнять инструкции Python.

Итак, начнем, пожалуй, с титульного изображения:

callscreen.kv:

<CallScreen>    FitImage:        id: title_image  # id для обращения к данному виджету        size_hint_y: .45  # высота изображения (45% от высоты экрана)        # Идентификатор root всегда ссылается на базовый класс.        # В нашем случае это <class 'uix.screens.baseclass.callscreen.CallScreen'>,        # а self - объект самого виджета - <kivymd.utils.fitimage.FitImage object>.        y: root.height - self.height  # положение по оси Y        source: "data/images/avatar.jpg"  # путь к изображению


Виджет FitImage автоматически растягивается на все выделенное ему пространство с сохранением пропорций изображения:


Можем запустить файл main.py и посмотреть результат:



Пока все просто и самое время приступить к анимированию виджетов. Добавим кнопку в экран по нажатию которой будут вызываться методы анимации из Python класса CallScreen:

callscreen.kv:

#:import get_color_from_hex kivy.utils.get_color_from_hex#:import colors kivymd.color_definitions.colors<CallScreen>    FitImage:        [...]    MDFloatingActionButton:        icon: "phone"        x: root.width - self.width - dp(20)        y: app.root.height * 45 / 100 + self.height / 2        md_bg_color: get_color_from_hex(colors["Green"]["A700"])        on_release:            # Вызов метода анимации титульного изображения.            root.animation_title_image(title_image); \            root.open_call_box = True if not root.open_call_box else False

Импорты модулей в Kv Language:

#:import get_color_from_hex kivy.utils.get_color_from_hex#:import colors kivymd.color_definitions.colors

Будут аналогичны следующим импортам в Python коде:

# Метод get_color_from_hex нужен дляпреобразования цвета# из шестнадцатеричной строки в формат rgba.from kivy.utils import get_color_from_hex# Словарь оттенков цветов различных цветовых схем:## colors = {#     "Red": {#         "50": "FFEBEE",#         "100": "FFCDD2",#         ...,#     },#     "Pink": {#         "50": "FCE4EC",#         "100": "F8BBD0",#         ...,#     },#     ...# }## https://kivymd.readthedocs.io/en/latest/themes/color-definitions/from kivymd.color_definitions import colors


После запуска и нажатия на зеленую кнопку получим AttributeError: 'CallScreen' object has no attribute 'animation_title_image'. Поэтому вернемся к базовому классу CallScreen в файле callscreen.py и создадим в нем метод animation_title_image, в котором будем анимировать титульное изображение.

callscreen.py:

# Класс для анимирования свойств виджетов.from kivy.animation import Animation[...]class CallScreen(MDScreen):    # Флаг для анимации возврата экрана к исходному состоянию.    open_call_box = False    def animation_title_image(self, title_image):        """        :type title_image: <kivymd.utils.fitimage.FitImage object>        """        if not self.open_call_box:            # Анимация развертывания титульного изображения на весь экран.            Animation(size_hint_y=1, d=0.6, t="in_out_quad").start(title_image)        else:            # Анимация возврата титульного изображения к исходному состоянию.            Animation(size_hint_y=0.45, d=0.6, t="in_out_quad").start(title_image)

Как вы уже поняли, класс Animation, наверное, как и в других фреймворках, просто анимирует свойство виджета. В нашем случае мы анимируем свойство size_hint_y подсказка высоты, задавая интервал выполнения анимации в параметре d duration и тип анимации в параметре t type. Мы можем анимировать сразу несколько свойств одного виджета, комбинировать анимации с помощью операторов +, += На изображении ниже показан результат нашей работы. Для сравнения для правой гифки я использовал типы анимаций in_elastic и out_elastic:

Следующий наш шаг добавить blur эффект к титульному изображению. Для этих целей в Kivy существует EffectWidget. Нам нужно установить нужные свойства для эффекта и поместить виджет титульного изображения в EffectWidget.

callscreen.kv:

#:import effect kivy.uix.effectwidget.EffectWidget#:import HorizontalBlurEffect kivy.uix.effectwidget.HorizontalBlurEffect#:import VerticalBlurEffect kivy.uix.effectwidget.VerticalBlurEffect#:import get_color_from_hex kivy.utils.get_color_from_hex#:import colors kivymd.color_definitions.colors<CallScreen>    EffectWidget:        effects:            # blur_value значение степени размытия.            (\            HorizontalBlurEffect(size=root.blur_value), \            VerticalBlurEffect(size=root.blur_value), \            )        FitImage:            [...]    MDFloatingActionButton:        [...]        on_release:            # Вызов метода анимации blur эффекта.            root.animation_blur_value(); \            [...]

Теперь нужно добавить атрибут blur_value в базовый класс Python CallScreen и создать метод animation_blur_value, который будет анимировать значение эффекта размытия.

callscreen.py:

from kivy.properties import NumericProperty[...]class CallScreen(MDScreen):    # Значение степени размытия для EffectWidget.    blur_value = NumericProperty(0)    [...]    def animation_blur_value(self):        if not self.open_call_box:            Animation(blur_value=15, d=0.6, t="in_out_quad").start(self)        else:            Animation(blur_value=0, d=0.6, t="in_out_quad").start(self)

Результат:


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

callscreen.py:

from kivy.utils import get_color_from_hexfrom kivy.core.window import Windowfrom kivymd.color_definitions import colors[...]class CallScreen(MDScreen):    [...]    def animation_call_button(self, call_button):        if not self.open_call_box:            Animation(                x=self.center_x - call_button.width / 2,                y=dp(40),                md_bg_color=get_color_from_hex(colors["Red"]["A700"]),                d=0.6,                t="in_out_quad",            ).start(call_button)        else:            Animation(                y=Window.height * 45 / 100 + call_button.height / 2,                x=self.width - call_button.width - dp(20),                md_bg_color=get_color_from_hex(colors["Green"]["A700"]),                d=0.6,                t="in_out_quad",            ).start(call_button)


callscreen.kv:

[...]<CallScreen>    EffectWidget:        [...]        FitImage:            [...]    MDFloatingActionButton:        [...]        on_release:            # Вызов метода анимации кнопки вызова.            root.animation_call_button(self); \            [...]



Добавим два пункиа типа TwoLineAvatarListItem на главный экран.

callscreen.kv:

#:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT#:import IconLeftWidget kivymd.uix.list.IconLeftWidget[...]<ItemList@TwoLineAvatarListItem>    icon: ""    font_style: "Caption"    secondary_font_style: "Caption"    height: STANDARD_INCREMENT    IconLeftWidget:        icon: root.icon<CallScreen>    EffectWidget:        [...]        FitImage:            [...]    MDBoxLayout:        id: list_box        orientation: "vertical"        adaptive_height: True        y: root.height * 45 / 100 - self.height / 2        ItemList:            icon: "phone"            text: "Phone"            secondary_text: "123 456 789"        ItemList:            icon: "mail"            text: "Email"            secondary_text: "kivydevelopment@gmail.com"    MDFloatingActionButton:        [...]        on_release:            root.animation_list_box(list_box); \            [...]



Мы создали два пункта ItemList и разместили их в вертикальном боксе. Можем создать новый метод animation_list_box в классе CallScreen для анимации этого бокса.

callscreen.py:

[...]class CallScreen(MDScreen):    [...]    def animation_list_box(self, list_box):        if not self.open_call_box:            Animation(                y=-list_box.y,                opacity=0,                d=0.6,                t="in_out_quad"б            ).start(list_box)        else:            Animation(                y=self.height * 45 / 100 - list_box.height / 2,                opacity=1,                d=0.6,                t="in_out_quad",            ).start(list_box)



Добавим панель инструментов в экран.

callscreen.kv:

[...]<CallScreen>    EffectWidget:        [...]        FitImage:            [...]    MDToolbar:        y: root.height - self.height - dp(20)        md_bg_color: 0, 0, 0, 0        opposite_colors: True        title: "Profile"        left_action_items:  [["menu", lambda x: x]]        right_action_items: [["dots-vertical", lambda x: x]]    MDBoxLayout:        [...]        ItemList:            [...]        ItemList:            [...]    MDFloatingActionButton:        [...]



Аватар и имя пользователя.

callscreen.kv:

[...]<CallScreen>    EffectWidget:        [...]        FitImage:            [...]    MDToolbar:        [...]    MDFloatLayout:        id: round_avatar        size_hint: None, None        size: "105dp", "105dp"        md_bg_color: 1, 1, 1, 1        radius: [self.height / 2,]        y: root.height * 45 / 100 + self.height        x: root.center_x - (self.width + user_name.width + dp(20)) / 2        FitImage:            size_hint: None, None            size: "100dp", "100dp"            mipmap: True            source: "data/images/round-avatar.jpg"            radius: [self.height / 2,]            pos_hint: {"center_x": .5, "center_y": .5}            mipmap: True    MDLabel:        id: user_name        text: "Irene"        font_style: "H3"        bold: True        size_hint: None, None        -text_size: None, None        size: self.texture_size        theme_text_color: "Custom"        text_color: 1, 1, 1, 1        y: round_avatar.y + self.height / 2        x: round_avatar.x + round_avatar.width + dp(20)    MDBoxLayout:        [...]        ItemList:            [...]        ItemList:            [...]    MDFloatingActionButton:        root.animation_round_avatar(round_avatar, user_name); \        root.animation_user_name(round_avatar, user_name); \        [...]



Типичное анимирование позиций X и Y аватара и имени пользователя.

callscreen.py:

[...]class CallScreen(MDScreen):    [...]    def animation_round_avatar(self, round_avatar, user_name):        if not self.open_call_box:            Animation(                x=self.center_x - round_avatar.width / 2,                y=round_avatar.y + dp(50),                d=0.6,                t="in_out_quad",            ).start(round_avatar)        else:            Animation(                x=self.center_x - (round_avatar.width + user_name.width + dp(20)) / 2,                y=self.height * 45 / 100 + round_avatar.height,                d=0.6,                t="in_out_quad",            ).start(round_avatar)    def animation_user_name(self, round_avatar, user_name):        if not self.open_call_box:            Animation(                x=self.center_x - user_name.width / 2,                y=user_name.y - STANDARD_INCREMENT,                d=0.6,                t="in_out_quad",            ).start(self.ids.user_name)        else:            Animation(                x=round_avatar.x + STANDARD_INCREMENT,                y=round_avatar.center_y - user_name.height - dp(20),                d=0.6,                t="in_out_quad",            ).start(user_name)



Нам осталось создать бокс с кнопками:



На момент написания статьи я столкнулся с тем, что в библиотеке KivyMD не обнаружилось нужной кнопки. Пришлось по-быстрому смастерить её самому. Я просто добавил в существующий класс MDIconButton инструкции canvas, в которых определил окружность вокруг кнопки, и поместил ее вместе с меткой в вертикальный бокс.

callscreen.kv:

<CallBoxButton@MDBoxLayout>    orientation: "vertical"    adaptive_size: True    spacing: "8dp"    icon: ""    text: ""    MDIconButton:        icon: root.icon        theme_text_color: "Custom"        text_color: 1, 1, 1, 1        canvas:            Color:                rgba: 1, 1, 1, 1            Line:                width: 1                circle:                    (\                    self.center_x, \                    self.center_y, \                    min(self.width, self.height) / 2, \                    0, \                    360, \                    )    MDLabel:        text: root.text        size_hint_y: None        height: self.texture_size[1]        font_style: "Caption"        halign: "center"        theme_text_color: "Custom"        text_color: 1, 1, 1, 1[...]



Далее мы создаем бокс для размещения кастомных кнопок.

callscreen.kv:

<CallBox@MDGridLayout>    cols: 3    rows: 2    adaptive_size: True    spacing: "24dp"    CallBoxButton:        icon: "microphone-off"        text: "Mute"    CallBoxButton:        icon: "volume-high"        text: "Speaker"    CallBoxButton:        icon: "dialpad"        text: "Keypad"    CallBoxButton:        icon: "plus-circle"        text: "Add call"    CallBoxButton:        icon: "call-missed"        text: "Transfer"    CallBoxButton:        icon: "account"        text: "Contact"[...]



Теперь созданный CallBox размещаем в правиле CallScreen и устанавливаем его положение по оси Y за нижней границей экрана.

callscreen.kv:

[...]<CallScreen>    EffectWidget:        [...]        FitImage:            [...]    MDToolbar:        [...]    MDFloatLayout:        [...]        FitImage:            [...]    MDLabel:        [...]    MDBoxLayout:        [...]        ItemList:            [...]        ItemList:            [...]    MDFloatingActionButton:        root.animation_call_box(call_box, user_name); \        [...]    CallBox:        id: call_box        pos_hint: {"center_x": .5}        y: -self.height        opacity: 0


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

callscreen.py:

from kivy.metrics import dp[...]class CallScreen(MDScreen):    [...]    def animation_call_box(self, call_box, user_name):        if not self.open_call_box:            Animation(                y=user_name.y - call_box.height - dp(100),                opacity=1,                d=0.6,                t="in_out_quad",            ).start(call_box)        else:            Animation(                y=-call_box.height,                opacity=0,                d=0.6,                t="in_out_quad",            ).start(call_box)



Финальная GIF-ка с тестом на мобильном устройстве:



На этом все, надеюсь, был полезен!
Подробнее..

Разработка мобильных приложений на Python. Создание анимаций в Kivy. Part 2

23.11.2020 08:20:36 | Автор: admin

Приветствую всех любителей и знатоков языка программирования Python!

Сегодня продолжим разбираться с темой анимаций в кроссплатформенном фреймворке для с поддержкой мультитач Kivy в связке с библиотекой компонентов Google Material Design KivyMD. В прошлой статье мы уже разбирали пример тестового приложения на Python/Kivy/KivyMD, в этой пройдемся по теме анимаций более подробно. В конце статьи я приведу ссылку на репозиторий проекта, в котором вы сможете скачать и сами пощупать, демонстрационное Kivy/KivyMD приложение. Как и предыдущая, эта статья будет содержать не маленькое количество GIF анимаций и видео, а поэтому наливайте кофе и погнали!

Kivy работает на Linux, Windows, OS X, Android, iOS и Raspberry Pi. Вы можете запустить один и тот же код на всех поддерживаемых платформах без внесения дополнительных изменений в кодовую базу. Kivy поддерживает большое количество устройств ввода, включая WM_Touch, WM_Pen, Mac OS X Trackpad и Magic Mouse, Mtdev, Linux Kernel HID, TUIO и так же как и Flutter, не задействует нативные элементы управления. Все его виджеты настраиваются. Это значит, что приложения Kivy будут выглядеть одинаково на всех платформах. Но благодаря тому, что виджеты Kivy могут быть кастомизированы как угодно, вы можете создавать свои собственные виджеты. Например, так появилась библиотека KivyMD. Прежде чем продолжить, давайте посмотрим небольшой обзор возможностей Kivy:

Демонстрационные ролики Kivy приложений






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

from kivy.animation import Animationfrom kivy.lang import Builderfrom kivymd.app import MDAppKV = """<CommonLabel@MDLabel>    opacity: 0    adaptive_height: True    halign: "center"    y: -self.heightMDScreen:    on_touch_down: app.start_animation()    CommonLabel:        id: lbl_1        font_size: "32sp"        text: "M A R S"    CommonLabel:        id: lbl_2        font_size: "12sp"        text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit""""class TestAnimation(MDApp):    def build(self):        return Builder.load_string(KV)    def start_animation(self):        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        Animation(            opacity=1, y=lbl_1.height * 2, d=0.9, t="in_out_back"        ).start(lbl_1)        Animation(            opacity=1, y=lbl_2.height + ids.lbl_1.height, d=1, t="in_out_back"        ).start(lbl_2)TestAnimation().run()

Это уже готовое приложение. Мы будем его лишь слегка редактировать. Правило CommonLabel в KV строке аналогично созданию класса в Python коде. Сравните:


Код в Kivy Language всегда короче и читабельнее. Поэтому в Python коде у нас будет только логика. Мы создали две метки с общими свойствами, описанными в правиле CommonLabel: прозрачность (opacity), размер текстуры метки (adaptive_height), горизонтальное выравнивание (halign), положение по оси Y (y ) и дали этим меткам id-шники (lbl_1, lbl_2), чтобы иметь возможность обращаться к свойствам объектов меток и манипулировать ими из Python кода. Далее мы привязали к событию on_touch_down (сработает при прикосновении к экрану в любом месте) вызов метода start_animation, в котором будем анимировать наши две метки.

Animation


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

    def start_animation(self):        # Получаем объекты меток из KV разметки        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        # Анимация первой метки        Animation(            opacity=1,  # анимация прозрачности до значения 1            y=lbl_1.height * 2,  # анимация положения виджета по оси Y            d=0.9,  # время выполнения анимация            t="in_out_back"  # тип анимации        ).start(lbl_1)  # в метод start передаем объект, который нужно анимаровать        # Анимация второй метки        Animation(            opacity=1, y=lbl_2.height + lbl_1.height, d=1, t="in_out_back"        ).start(lbl_2)

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

  1. in_out_back
  2. out_elastic
  3. linear


Давайте немного усложним задачу и попробуем анимировать вращение меток на плоскости. Для этого будем использовать матричные манипуляции (PushMatrix, PopMatrix, Rotate, Translate, Scale). Добавим к общей метке инструкции canvas:

<CommonLabel@MDLabel>    angle: 180  # значение вращения    [...]    canvas.before:        PushMatrix        Rotate:            angle: self.angle            origin: self.center    canvas.after:        PopMatrix

А в Python коде в класс Animation передадим новое свойство angle для анимации:

    def start_animation(self):        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        Animation(angle=0, [...]).start(lbl_1)        Animation(angle=0, [...]).start(lbl_2)

Результат:

Добавим анимирование масштаба меток:

<CommonLabel@MDLabel>    scale: 5  # значение масшбирования    [...]    canvas.before:        PushMatrix        [...]        Scale:            # масштабирование по трем осям            x: self.scale            y: self.scale            z: self.scale            origin: self.center    canvas.after:        PopMatrix

В Python коде в класс Animation передадим новое свойство scale для анимации:

    def start_animation(self):        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        Animation(scale=1, [...]).start(lbl_1)        Animation(scale=1, [...]).start(lbl_2)

Результат:

Класс Animation имеет ряд событий для отслеживания процесса анимации: on_start, on_progress, on_complete. Рассмотрим последний. on_complete вызывается в момент завершения процесса анимации. Привяжем это событие к методу complete_animation, который мы сейчас создадим:

[...]class TestAnimation(MDApp):    [...]    def complete_animation(self, animation, animated_instance):        """        :type animation: <kivy.animation.Animation object>        :type animated_instance: <WeakProxy to <kivy.factory.CommonLabel object>>        """        # Анимируем масштаб и цвет первой метки.        Animation(scale=1.4, d=1, t="in_out_back").start(animated_instance)        Animation(color=(1, 0, 1, 1), d=1).start(animated_instance)

Привязываем событие:

    def start_animation(self):        [...]        animation = Animation(            angle=0, scale=1, opacity=1, y=lbl_1.height * 2, d=0.9, t="in_out_back"        )        animation.bind(on_complete=self.complete_animation)        animation.start(lbl_1)        [....]

Результат:

На этом пока все. Просили:

Ниже прикрепляю превью Kivy/KivyMD проекта и ссылку на репозиторий, где можно скачать APK и пощупать:

Репозиторий github.com/HeaTTheatR/Articles
APK можно найти в директории репозитория StarTest/bin
Подробнее..

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

22.11.2020 00:21:34 | Автор: admin

Всем здравствуйте!


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


Должен сразу оговориться, что больше рассказ коснётся KnownReader (он же KR), который мы разрабатываем, а также близкого семейства, построенного на одном движке (CoolReader, он же CR; KOReader). Но кое-какие ссылки и на другие приложения будут даны, если потребуется. Хотелось не столько поделиться собственными ноу-хау, сколько рассказать, как оно работает изнутри, с какими проблемами пришлось столкнуться и как они были решены, а заодно и познакомить читателя с интерфейсом и возможностями приложения. Потому что читалка это целая философия, без понимания которой можно не заметить целый океан возможностей, скрытых от взгляда обывателя.


Выделяем, переводим


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


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


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


Направление перевода
Направление перевода Перевод (Lingvo)

Про элементы управления


Должен сделать небольшое отступление о том, как устроены элементы управления в программах для чтения. Читалка вещь уникальная тем, что она должна быть похожа на книгу. Поэтому на всём своём пространстве она рисует текст. А если везде текст, как же ей управлять? И вот что обычно предлагают читалки:
Тап-зоны это зоны экрана (в KR их 9 3х3), на нажатие которых можно назначить действия. Очевидно, должны быть как минимум следующая/предыдущая страницы, но не только. Разнообразие действий очень велико, а потому тут каждый развлекается как может. Буквально. В настройках KR можно настроить тап-зоны под себя;


Тап зоны


Жесты. Всякие смахивания, проведения слева направо, сверху вниз и т.п.;
Панель с кнопками. Её не все любят, считая её визуальным мусором, но не все и не любят.


Про ноу-хау


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


  1. В качестве словаря в KR можно указать любую программу, которая может принимать в себя текст (хоть СМС отправляйте). Это удобно, если у вас есть какой-то словарь, про который KR не знает. Отмечу также, что и переключение словарей также можно вынести на тап-зону;
  2. Сделали несколько настроек словарей, между которыми можно быстро переключаться (режимы переключить на 2 словарь, переключить на 2 словарь на один следующий перевод, потом вернуться обратно);

Словари и режимы выделения
Настройки словарей Настройки различных видов выделения в разных режимах: режим чтения, выделения, инспектора

  1. Добавили уникальное действие длинный двойной тап. Это когда вы делаете двойной тап, но не отпускаете палец после второго тапа. На это уникальное действие можно настроить что душе угодно. Например, вызов другого словаря. Если вы знаете другую читалку, которая умеет длинный двойной тап, напишите об этом в комментариях или в личку. Мне правда интересно;
  2. К жестам (это не про словари, но все равно напишу) добавили возможность менять размер шрифта диагональным щипком и размеры полей горизонтальным и вертикальными щипками. Тут следует отметить, что аудитория читалки отреагировала хорошо. Но не вся аудитория. А потому без правок не обошлось. В следующем же релизе пришлось вынести в настройки возможность отключения управления жестами;
  3. Разнообразили панель с кнопками её теперь можно очень гибко настраивать: изменять список и функции кнопок, регулировать её размер (больше, меньше, средний) и цвет. Вообще, гибкость настраивания это про KR. Возможно, кого-то это может запутать поначалу, но было замечено, что к такой свободе люди быстро привыкают и неохотно от неё отказываются.

Несколько словарей


Одна из моих любимых тем.


Минутка заботы от автора материала

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


  1. Элемент управления или пункт меню в читалке;
  2. Приложение для отображения словарей (Abbyy, GoldenDict и т.д.);
  3. Конкретный словарь (например Толковый словарь Ушакова), отображаемый в пункте выше.
    Как правило, из контекста понятно о чём идет речь. Если непонятно, спрашивайте, отвечу.

Как правило, хватает двух словарей. Но в KR их больше. И вот что это даёт:


  1. Разнообразие. В одной программе могут быть одни словарные статьи, в другой другие. Где-то есть транскрипция, где-то нет. Какие-то умеют переводить фразы (Яндекс, Гугл, Промт), какие-то могут оперировать только словами.
  2. Словари языковые и толковые. Может быть интересно посмотреть как перевод слова, так и его толкование (википедия, энциклопедии и т.д.).

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


  • Lingvo. Очень достойное приложение от уважаемой фирмы ABBYY. Всё на уровне, даже с проговаривателем. Умеем показывать его в карточке (т.е. на часть экрана). Есть хороший такой минус это платно. Как сама программа, так и словари. Я покупал английский словарь за 20 долларов (а для России ещё и с НДС). Другой значимый минус в том, что нельзя повлиять на развитие. Например, я просил поддержку добавить кнопку скопировать словарную статью уж столько воды утекло, а её до сих пор не добавили.
  • GoldenDict. Очень хорошая вещь, практически не имеющая аналогов, у которой есть куча словарей. Их очень много. Также GoldenDict умеет найти словари из Lingvo и использовать их. Есть форк на форуме 4pda, который привносит некоторую функциональность и осовременивает интерфейс. От минусов совсем грустно становится развитие остановилось, исходников программы нет. Да и платный он, этот GoldenDict.
  • Aard 2. Совершенно уникальная вещь. Программа создана для обработки больших (даже не больших, а огромных) словарей. На сайте Aardа выкладываются дампы википедий. Представляете, оффлайновая википедия в телефоне? Ух...

Любопытный вариант использования Aard

Аард один раз лично мне очень помог в работе (в настоящей, за которую деньги платят). У него есть утилита Slob (на питоне), которая позволяет выполнять поиск словарных статей и выгружать их в html формат. Нам (моей команде на работе) нужно было обогатить информацию о населённых пунктах РФ данными из википедии (численность населения по годам и т.д.). Мы разработали свою утилиту, которая запускала Slob и давала ей на вход название населенного пункта (например Пермь). Далее находились все статьи (с околопермской тематикой), после чего мы пакетно обрабатывали файлы и легко вычисляли нужный (через jsoup) по наличию определённых атрибутов коды ОКАТО, ОКТМО и т.д., затем забирали из него нужные данные (страницы википедии хорошо шаблонизированы и структурированы). Так задача была решена в автоматическом режиме по всем населённым пунктам РФ.


  • Яндекс Переводчик / Гугл Переводчик. Это уже новое веяние, они в представлении не нуждаются. Работают, развиваются, очень хороши. Недавно, правда, Яндекс немного расстроили, переведя свой API переводчика на платные рельсы. Мы об этом писали на форуме и даже инструкцию оформили о том, как его теперь готовить для использования в KR. Поэтому здесь повторяться не буду. Если кому интересно станет, заходите, в нашей ветке на форуме уютно.
  • Если знаете другие хорошие словари добавляйте в комментарии, обсудим.

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


Немного многословно, но до сих пор мы с вами обсудили только базовую функциональность выделили слово, перевели слово (возможно, даже в нескольких словарях и с множеством хитрых настроек). Хорошо, но мало. Ведь есть нерешённая проблема. При чтении нам иногда встречаются трудные слова, которые мы переводим. Эти слова могут встречаться и на следующих страницах, а мозг устроен таким образом, что сразу слово можно и не запомнить. А потому одно и то же действие мы вынуждены повторить несколько раз. Чтобы этого не делать мы добавили то, что называем пользовательским словарём. Это наше нововведение и в какой-то мере отличительная особенность.
Сейчас расскажу, как оно работает. Мы завели небольшую панель под книгой, в которую помещаем слова из пользовательского словаря (до 10 слов), встретившиеся на текущей (читаемой) странице. Можно быстро нажать и подсмотреть перевод.


Онлайн-словарь
Перевод в онлайн-словаре Перевод в онлайн-словаре

Пользовательский словарь
Сохранённые слова в пользовательском словаре Быстрый просмотр слова из пользовательского словаря

Цитаты, история поиска
Диалог пользовательский словарь / цитаты / история поиска в словаре Диалог пользовательский словарь / цитаты / история поиска в словаре

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


  1. При выделении слова оно запоминается внутри KR как последнее выделенное слово.
  2. Дальше оно уходит во внешнюю программу-словарь.
  3. Мы в свою очередь прочитали, посмотрели, дальше ВДЕЛИЛИ словарную статью и СКОПИРОВАЛИ её в буфер обмена, затем вернулись в KR и нажали действие (мы же помним, что его можно назначить на тап-зону) создать элемент пользовательского словаря по последнему выделению и содержимому буфера обмена (знаем, что надо поработать над формулировкой, но как есть), после чего осталось только нажать OK. Можно даже без буфера есть возможность отправить текст в KR через всплывающее меню андроида функциональность PROCESS_TEXT.
    Что здесь сложно? Копирование словарной статьи. Это снова подержать, подвигать ползунки (не у нас, а в программе-словаре). Хорошо, мы упорные. Поговорили с коллегами из GoldenDict mod, сделали для них доработку добавили кнопку в GoldenDict, чтобы можно было хотя бы выделить словарную статью. Коллеги обещали нашу доработку включить в свой билд и выпустить. Ждём-с.

Онлайн словари


Пользовательский словарь это не только нижняя панелька. Это полноценное окно пользовательский словарь / цитаты / история поиска в словаре. С поиском и т.д. Вызывается также по действию, либо можно нажать на ссылку слов: Х.
Вроде бы и звучит хорошо, но мало. Мы живем в современном мире. Современный мир живет в онлайне. Поэтому тут мы добавили онлайновые словари Яндекс, Lingvo, Wikipedia (пока еще в ранней реализации).:


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

Режимы


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


  1. Режим выделения.
    Включается режим выделения (страница текста обводится сплошной рамочкой), в котором слово можно выделить в один тап, а несколько слов тапнули и провели. Что мы поменяли. В режиме выделения можно настроить другие словари на действия. Действий три: выделение слова, выделение нескольких слов, выделение слова по длинному двойному тапу. Это расширяет возможности использования выделения. Допустим в обычном режиме перевод в англо-русском словаре, в режиме выделения в википедии.
  2. Режим инспектора.
    Страница текста выделяется штрихпунктирной рамочкой. Режим выделения после перевода отключается. Режим инспектора это такой залипающий режим выделения, который включен, пока не выключен. Удобно, например, если хочется протыкать много слов на странице друг за другом и получить быстрый перевод по каждому. Также в режиме инспектора тоже можно настроить свои словари (на три варианта выделения).

Режимы
Режим выделения Режим инспектора

Википедия
Поиск в Википедии список статей Википедия выбранная статья

Что дальше?


Много всего сказано, но хочется большего:


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

Отображение двух страниц


  • Пользовательский словарь. Сейчас найденные слова на странице отрисовываются на нижней панельке. Есть желание просто подсвечивать их в тексте в виде примерно как гиперссылка.
  • Лингво-онлайн умеет отдать wav-файл с озвучкой. У нас пока не реализовано, но можно сделать проговариватель.
  • Википедия-онлайн у нас выводится без форматирования. Можно развиваться в эту сторону.
    К чему это я? Да к тому, что приложение живёт, пока оно кому-нибудь нужно. Желательно, чтобы оно было нужно не только его автору и паре-тройке его друзей. Нет, это, конечно, тоже уже кое-что, но всегда хочется большего. Если вы посетите нашу ветку на 4pda, то, возможно, заметите, что у нас ведётся достаточно активное обсуждение новых фич и старых багов. У нас есть своя аудитория, мы ценим её мнение и дорожим ей. Но взгляд со стороны довольно часто бывает полезным. А потому приглашаю вас, хабровчане, к обсуждению.
Подробнее..

Как исправить баг с Drawable.setTint в API 21 Android SDK

09.11.2020 18:08:51 | Автор: admin

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

В 21 версии API Android SDK появился универсальный инструмент изменения цвета для всех Drawable - Drawable.setTint(int color). Но как раз-таки в этой самой версии он не работает у некоторых наследников Drawable, а именно GradientDrawable, InsetDrawable, RippleDrawable и всех наследников DrawableContainer.

Если посмотреть в исходники API 21, скажем, GradientDrawable (прямого наследника Drawable), мы не найдем переопределенного метода setTint и его вариаций. А это значит, что в данной реализации разработчики попросту не поддержали эту функцию.

Проблему условно решили в библиотеке обратной совместимости. Сейчас ее можно найти по артефакту androidx.core:core. Чтобы поддержать tinting на версиях 14-22, были созданы обертки WrappedDrawableApi14 и WrappedDrawableApi21. Последняя является наследницей первой и, по сути, не несет логики по поддержке окрашивания.

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

final ColorStateList tintList = mState.mTint;final PorterDuff.Mode tintMode = mState.mTintMode;if (tintList != null && tintMode != null) {   final int color = tintList.getColorForState(state, tintList.getDefaultColor());   if (!mColorFilterSet || color != mCurrentColor || tintMode != mCurrentMode) {       setColorFilter(color, tintMode);       mCurrentColor = color;       mCurrentMode = tintMode;       mColorFilterSet = true;       return true;   }} else {   mColorFilterSet = false;   clearColorFilter();}

Данный кусок кода будет вызываться каждый раз при вызове Drawable.setState(int[] stateSet).

При использовании этих оберток вы теряете возможность вызывать специфические методы для конкретных Drawable. Так, например, при оборачивании GradientDrawable вы не сможете управлять градиентом, так как обертка в своем интерфейсе не имеет методов таких, как setShape, setGradientType и.т.п. Чтобы получить доступ к данным методам, обернутый Drawable придется развернуть (DrawableCompat.unwrap(Drawable)). Но в таком случае вы теряете тинт. Если он у вас состоял только из одного цвета, ничего страшного, ведь этот цвет сохранится как цветовой фильтр в оригинальном Drawable. Но если тинт был stateful, цвета для стейтов, отличных от текущего, будут потеряны.

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

Если ваш тинт состоит лишь из одного цвета, вы можете в любой момент выполнить следующие действия:

val wrapped = DrawableCompat.wrap(drawable)wrapped.setTint(...)drawable = DrawableCompat.unwrap(wrapped)

После чего смело делать дальше свои дела.

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

class GradientDrawableWrapper(    val original: GradientDrawable,     var ColorStateList tint) {    fun get(): Drawable {        return wrap()    }    fun setShape(@Shape shape: Int) {        original.setShape(shape)    }    // other specific method proxies...    private fun wrap(): Drawable {        val wrapped = DrawableCompat.wrap(original)        wrapped.setTint(tint)        return wrapped    }}

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

Подробнее..

Перевод Студийные приложения Netflix на Android и iOS теперь с Kotlin Multiplatform

09.11.2020 20:17:29 | Автор: admin
Примечание от переводчика: при словах мобильные приложения Netflix вспоминаются их основные приложения для просмотра видеоконтента, но компания ведёт и киностудийную разработку для тех, кто этот контент создаёт. И их пост посвящён использованию Kotlin Multiplatform во втором случае.

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


Поскольку сетевое соединение часто оказывается ненадёжным, мы обратились к мобильным решениям для персистентности на клиентской стороне и поддержки офлайна. А потребность выпускать быстро привела к экспериментам с мультиплатформенной архитектурой. И теперь мы зашли тут ещё на шаг дальше, использовав Kotlin Multiplatform, чтобы писать платформонезависимую бизнес-логику один раз на Kotlin и компилировать её в Kotlin-библиотеку для Android и нативный Universal Framework для iOS с помощью Kotlin/Native.



Kotlin Multiplatform


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

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


Этот подход хорошо работает для нас по нескольким причинам:


  1. У наших приложений для Android и iOS общая архитектура со схожей, а порой и идентичной бизнес-логикой на обеих платформах.
  2. Почти 50% нашего продакшн-кода в наших Android- и iOS-приложениях не связано с платформой.
  3. Это никак не мешает нам изучать новые технологии от самих этих платформ (Jetpack Compose, Swift UI и так далее).

Итак, что мы с этим делаем?


Управление опытом (experience management)


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


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


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


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


Это привело нас к решению сделать легковесный мобильный SDK для Hendrix и он был отличным кандидатом для Kotlin Multiplatform, так как требует значимой бизнес-логики и полностью платформонезависим.


Реализация


Для краткости мы опустим конкретные детали о Hendrix и затронем отличия в использовании Kotlin Multiplatform от Kotlin/Swift.


Сборка


На Android всё как обычно. Hendrix Multiplaform SDK подключается с помощью Gradle в качестве Android-библиотеки как любая другая зависимость. В случае с iOS нативный бинарь включается в проект Xcode как универсальный фреймворк.


Эргономика разработки


В случае с Kotlin Multiplatorm исходный код можно редактировать, перекомпилировать и добавлять к нему отладчик с брейкпойнтами хоть в Android Studio, хоть в Xcode (включая поддержку lldb). Android Studio работает из коробки, поддержка Xcode достигается с помощью плагина xcode-kotlin от TouchLabs.



Отлаживаем Kotlin-исходники в Xcode


Работа с сетью


Hendrix интерпретирует набор правил удалённо конфигурируемые файлы, которые оказываются скачаны на устройство. Мы используем Multiplatform HttpClient из фреймворка Ktor, чтобы добавить наш код работы с сетью в SDK.


Дисковый кэш


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


Завершающие мысли


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


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


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


Мы обратили внимание на пост Netflix, потому что перекликающийся с ним доклад скоро представят на нашей конференции Mobius: там тоже расскажут об опыте внедрения Kotlin Multiplatform в продакшн крупной компанией. Только речь пойдёт не о малоизвестном нишевом приложении, а о суперпопулярных Яндекс.Картах. Если два таких гиганта почти одновременно заговорили о продакшн-опыте, значит ли это, что вот теперь время Kotlin Multiplatform пришло?
Подробнее..

Монетизация рекламного трафика в мобильной экосистеме Huawei

13.11.2020 16:21:32 | Автор: admin

Привет, Хабр! Работа со встроенной рекламой в приложениях на платформе Huawei Mobile Services ведётся с помощью сервиса Ads Kit. Сервис предоставляет пользователям персонализированную рекламу, позволяет разработчикам анализировать результаты промокампаний и работать с основными рекламными форматами. В статье я расскажу, что включает в себя этот сервис, какие рекламные форматы поддерживает и какие даёт возможности для аналитики. Кому интересно прошу под кат.

Что такое Ads Kit

Ads Kit включает в себя две службы: Ads Publisher Service и Identifier Service. Ads Publisher Service предоставляет инструменты для интеграции рекламных объявлений и получения отчётов (количества запрошенных и просмотренных объявлений, кликов). Identifier Service обеспечивает работу с рекламным идентификатором пользователя Open Advertising ID (OAID) и персонализацией объявлений.

Пока рекламная платформа доступна только для корпоративных аккаунтов, поэтому стоит помнить, что при работе с платёжными сервисами экосистема Huawei требует обязательную регистрацию юридического лица. Но скоро Ads Kit смогут внедрить в свои приложения все разработчики. Также необходимо учитывать, что Ads Kit работает с HMS Core (APK) 4.0.0, так как на предыдущих версиях возможны ошибки.

Форматы рекламы

Сейчас в Ads Kit доступно 5 разных типов рекламы:

  • Баннерная реклама. По размерам баннеры делятся на фиксированные и адаптивные. У фиксированных 5 размеров: 320 x 50 dp, 320 x 100 dp, 300 x 250 dp, 360 x 57 dp для изображений 1 080 x 170 пикселей и 360 x 144 dp для изображений 1 080 x 432 пикселя. Адаптивные рекламные баннеры автоматически регулируют ширину в зависимости от соотношения сторон устройств. Также есть возможность использовать смарт-баннеры при загрузке рекламы Huawei Ads SDK создаёт рекламный экран той же ширины, что и экран, в зависимости от его ориентации на устройстве.

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

  • Rewarded Ads. Реклама с вознаграждением это полноэкранные видеообъявления, которые награждают пользователей за просмотр. При просмотре рекламы сервер отправляет уникальный URL со специфичными параметрами в Ads Kit, чтобы уведомить медиасервер о том, что пользователь должен быть вознаграждён за взаимодействие с видеообъявлением. При принятии решения о том, когда вознаграждать пользователя, необходимо сбалансировать проверку пользователя и скорость подтверждения вознаграждения, чтобы, с одной стороны, защититься от спуфинга, а с другой не заставлять пользователя ждать своей награды.Разработчики Huawei рекомендуют сразу вознаграждать пользователя, обрабатывая запрос на стороне клиента, а потом проверять все вознаграждения при получении обратных вызовов на стороне сервера. Такой подход гарантирует действительность предоставленных вознаграждений, обеспечивая при этом хорошее взаимодействие с пользователем.

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

  • Splash Ads. Рекламные заставки отображаются сразу после запуска приложения, даже до того, как отобразится главный экран приложения. Для таких заставок может быть установлен таймер показа и реагирование на конкретные события, например когда пользователь перешёл по рекламной ссылке.

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

Для разных типов рекламы также можно проставить теги с возрастом и регионом. Например, согласно соглашению GDPR, действующему в рамках Европейской экономической зоны, нельзя показывать персонализированную рекламу и запрашивать право на её показ для пользователей, не достигших 16 лет,при этом некоторые страны устанавливают порог в 13 лет.

Рекламная аналитика

Анализ трафика и персонализация объявлений осуществляется с помощью службы Identifier Service. Она создаёт и работает с рекламным идентификатором пользователя (OAID). Пользователь может отключить функцию персонализированной рекламы или сбросить свой OAID. При сбросе OAID, если персонализированная реклама не отключена, будет создан новый номер. Если пользователь согласился на обработку своих данных и получение персонализированной рекламы, то разработчики, в свою очередь, получают возможность использовать его зашифрованный обезличенный OAID.

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

Для анализа источников трафика применяется функция Install Referrer, которая считывает информацию из реферальной ссылки, использованной пользователем для установки приложения. Чтобы использовать Install Referrer, требуется интегрировать API Ads Kit и после этого опубликовать приложение в AppGallery.

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

Подробнее..

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

15.11.2020 14:11:45 | Автор: admin
В этом дайджесте обсуждаем ужасную документацию Apple, запуск Android Studio на любом устройстве, переезд на Kotlin (в том числе и Multiplatform), создание бэкенда без серверных разработчиков, успех Among Us и UX-игры и многое другое.

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

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

iOS

(+6) Интеграция CI/CD для нескольких сред с Jenkins и Fastlane. Часть 2
(+5) Разница между @StateObject, @EnvironmentObject и @ObservedObject в SwiftUI
(+5) SPM: модуляризация проекта для увеличения скорости сборки
Apple выпускает macOS Big Sur
Apple показала первые компьютеры на Apple Silicon
iOS 14.3 предлагает установку сторонних приложений при настройке системы
В новой версии TestFlight появилось автоматическое обновление приложений
Многие издатели отказались от публикации iOS-приложений в Mac App Store
На пути к изучению SwiftUI
3 подхода к созданию эффекта размытия в iOS
SwiftUI против реактивной MVVM-архитектуры
Анимированная круговая диаграмма на SwiftUI 2 с использованием Combine
Перенос существующего проекта Xcode на Kotlin Multiplatform Mobile
Почему новые Mac кардинально меняют правила игры для продуктовых дизайнеров?
Использование кривых Безье в SwiftUI
iPhoneNumberField: поле ввода телефона на SwiftUI
MarqueeText: бегущая строка

Android

(+19) Студийные приложения Netflix на Android и iOS теперь с Kotlin Multiplatform
(+18) Переезд из Java в Kotlin: как забрать коллекции с собой
(+9) Jetpack Compose Desktop
(+3) Как исправить баг с Drawable.setTint в API 21 Android SDK
Как запустить Android Studio на ЛЮБОМ устройстве с помощью JetBrains Projector
Видео Chicago Roboto 2020
Android Broadcast: Jetpack Compose на Desktop
Как сделать компилятор умнее
Разработка приложений для Android: полезные инструменты для разработчиков
Android Navigation с помощью уже существующего обратного стека
Разработка для разработчиков: работа над Android-библиотекой
Android RecyclerView: часть 1 Создание простого списка
Как упростить делегирование Android View Binding
Android TV: лучшие практики увлекательных приложений
Ускорение сборки с помощью Dagger Reflect
Классы данных отличный способ хранения данных
iiVisu: визуализатор звука для Android
Brackeys IDE: редактор кода для Android
ForgetMeNot: запоминание с помощью карточек

Разработка

(+24) Как приручить Charles Proxy?
(+22) Meta Gameplay Framework, или бэкенд без серверных разработчиков
(+13) Дополнительные компоненты для кроссплатформеннной библиотеки материального дизайна KivyMD
(+11) Лучшие выпускные работы весеннего семестра Технопроектов
(+9) Собеседование для QA: резюме, вопросы на интервью, переговоры о зарплате + полезные ссылки
(+8) EventBus Система событий для Unity
(+7) Система сделал-измерил-узнал
(+7) Улучшая интерфейс: как связаны дизайн и успех продукта
(+4) Разворачиваем сервер для проверки In-app purchase за 60 минут
(+3) html2json
Radio QA #62: удалённые конференции.
Podlodka #189: IoT
Минцифры готовит новый пакет поддержки IT-отрасли
Производители просят отложить предустановку российского ПО
Дизайн приложений: примеры для вдохновения #22
6 небольших проектов, которые прокачают ваше резюме разработчика
В ноябрьском рейтинге TIOBE впервые на 2 место вышел Python
Как я выпустил своё приложение в App Store без знаний кода
Рекомендации по проектированию интерфейса смарт-часов
Как начать работать на React Native, чтобы не было мучительно больно
Разбираем UX популярной игры Among Us
Влияние производительности мобильного приложения на пользовательский опыт
Практические советы Junior-разработчикам
Основные уроки, извлеченные из работы с 10х разработчиком
Глубокие ссылки в Flutter с помощью Branch
Практики, которые удвоили мою продуктивность как разработчика
Внимание мой самый ценный актив продуктивности как разработчика
Лучшие практики документирования кода для программистов
Лучшие мобильные приложения для обучения программированию на ходу
React Native в Wix архитектура
Отличный Code Review суперсила, которая нужна вашей команде
Различные способы использования цвета в дизайне
4 лучших расширения VS Code в 2020 году

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

(+26) Динамический лут в играх: что стоит учитывать
(+6) Монетизация рекламного трафика в мобильной экосистеме Huawei
(+6) Ролики-мислиды: почему они работают?
(+3) Как локализовать игру? Пошаговое руководство
App Annie: что ждет мобильный рынок в 2021 году
Успех Among Us: исследование циклов роста игры, поразившей мир
Fortnite может получить ежемесячную подписку
MeowTalk: кошачий переводчик
Ushur: эффективное общение с клиентами
6 типичных ошибок при запуске мобильного приложения

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

(+43) Как программировать многоядерные микроконтроллеры
(+20) Маркетинговые хитрости на рынке смартфонов. Где нет прогресса?
(+11) Под капотом: 4D радар для построения изображений от Vayyar
(+8) Медленный CrossWorks for ARM?
(+8) Как настроить сбор данных с датчиков IoT и SCADA для Data Governance
Hyundai Motor выбирает платформу NVIDIA DRIVE

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

В тюрьму за приложение

20.11.2020 16:08:20 | Автор: admin

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

Днём 23 октября С. Ювараджа был в своем доме в Тирупуре и писал программное обеспечение, когда в его дверь постучали сотрудники полиции.

Вы разработали это приложение? спросили полицейские, указывая на экран телефона с телефонным приложением Super Tatkal, которое позволяет заказывать железнодорожные билеты намного быстрее, чем через IRCTC, громоздкий веб-сайт Индийских железных дорог. Услышав в ответ да, они арестовали 32-летнего разработчика в соответствии с Законом об Индийских железных дорогах 1989 года за несанкционированный бизнес по приобретению и поставке железнодорожных билетов. Вскоре после его ареста чиновники также добились деактивации Super Tatkal и Super Tatkal Pro, которые до этого были доступны в магазине Google Play.

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

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

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

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

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

Немного о разработчике и предмете спора

Ювараджа впервые начал программировать на BASIC в 11 лет. Вскоре он полюбил кодинг и начал рассматривать это направление как вариант карьеры, когда познакомился с Java. В 2007 году Ювараджа поступил на авиационного инженера в Технологический институт Мадраса, ведущую техническую школу Тамил Наду. Будучи весьма перспективным учащимся, он получал стипендию в размере 3000 рупий (3080 рублей) в месяц от Организации оборонных исследований и разработок правительства Индии (DRDO). В Массачусетском технологическом институте в Ченнаи Ювараджа встретил таких же, как он. Тех, кто любит программировать. Позднее он получил степень магистра в области авиационной техники и устроился в хорошую авиационную фирму в Бангалоре.

Работая в Бангалоре, Ювараджа часто заказывал билеты для поездки домой в Тирупур. Именно тогда я понял, что приложение IRCTC ужасно медленное. Кроме того, я всегда хотел разрабатывать приложения, и мне показалось, что сейчас самое подходящее время , рассказывает Ювараджа. Так появился Super Tatkal.

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

Сначала в приложении были баги, но постоянная группа пользователей давала обратную связь, указывая на недочёты. Устраняя их, Ювараджа понял, что приложение набирает популярность. Первый год оно было бесплатным. Позже, когда стоимость обслуживания облачных серверов увеличилась до 10 000 рупий в месяц, разработчик попросил у пользователей финансовой поддержки. Это не помогло, и тогда он решил ввести систему покупок внутри приложения, когда пользователь мог купить стопку из 10 виртуальных монет за 20 рупий (около 20 рублей). После первых трёх бесплатных бронирований приложение просило пять монет за транзакцию, то есть пользователь должен был заплатить 10 рупий (10 рублей) за бронирование.

Железнодорожники утверждают, что Ювараджа заработал 20 миллионов рупий через Super Tatkal в период с 2016 по 2020 год. Это может показаться большим числом, но получается около 40 000 рупий в месяц. При этом все пользователи знали о модели оплаты и что разработчик никого не обманывал, заставляя платить.

Мы не разрешали

Super Tatkal ProSuper Tatkal Pro

Так как Super Tatkal набирал обороты, Ювараджа решил погрузиться в мир стартапов. Он уволился с работы в 2017 году и вместе с двумя своими друзьями создал приложение-агрегатор грузовиков Speedbird trucks. Это было похоже на Ola или Uber для грузовиков. Но у компании быстро появились проблемы. Было сложно убедить людей использовать грузовики, доступные в приложении, потому что существующие контракты с известными сервисами казались пользователям проще. Компания была основана на сбережениях трех молодых людей, и ей не хватало бизнес-модели. История закончилась убытками.

Между тем у Super Tatkal дела шли относительно хорошо. Количество пользователей продолжало расти, и было запущено второе приложение, Super Tatkal Pro. Оно позволило бронировать билеты через приложение IRCTC для железнодорожного сообщения в 2019 году. Чтобы поддерживать свой стартап и поддерживать свою семью, Ювараджа также работал программистом-фрилансером. Доход был небольшой, но стабильный. Однако Ювараджа и не подозревал, что его приложение внимательно изучалось сотрудниками железнодорожной полиции.

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

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

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

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


Что ещё интересного есть в блогеCloud4Y

Найдено давно утерянное руководство к самому старому компьютеру в мире

Детям о Кубернете, или приключения Фиппи в космосе

Определённо не Windows 95: какие операционные системы поддерживают работу в космосе?

Рассказываем про государственные защищенные сервисы и сети

Внутри центра обработки данных Bell Labs, 1960-е

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

Подробнее..

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

22.11.2020 14:12:21 | Автор: admin
В новом выпуске путь к 10х инженеру, снижение комиссии App Store, тестирование без доступа к коду, руководство по росту приложений, как работает компьютерное зрение, выбор кроссплатформенной технологии и многое другое!



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

iOS

(+9) На пути к 10x инженеру: шорткаты, сниппеты, шаблоны
(+6) Впечатление от Стэнфордских курсов CS193P Весна 2020 г.: Разработка iOS приложений с помощью SwiftUI
(+1) Composable Architecture свежий взгляд на архитектуру приложения. Тесты
(0) Keychain API в iOS
NVIDIA GeForce Now и Google Stadia запускаются на iOS в виде веб-приложений
Apple выпустила версию TensorFlow для macOS Big Sur
Apple снижает комиссию App Store для небольших разработчиков до 15%
В App Store заработали промо-коды для подписок
Использование Kotlin Multiplatform Mobile в iOS-проекте
Как безопасно работать с Core Data в фоновом потоке
Общие потоки, широковещательные каналы
Работа с Diffable Data Sources в iOS 14
Серверный Swift с Vapor, AWS Fargate и AWS Cloud Development Kit
Внедрение зависимостей: почему мы выбрали Needle
Главные шорткоды Xcode для более эффективного программирования
MVVM в iOS с SwiftUI (подробный пример + подводные камни)
Создавайте удобные для контента макеты
Создание интерфейса чата с помощью SwiftUI
Жизненный цикл SwiftUI View
Полное руководство по использованию Azure CI/CD для iOS-приложений
SlideOverCard: выдвигающаяся карточка
PostgresKit: работа с PostgreSQL

Android

(+30) Корутины и синхронизация. Лучше не смешивать
(+4) Как устроен билд APK файла внутри
(+3) Android-разработка: что общего с Lego и как не натягивать сову на глобус
Роман Елизаров становится новым руководителем проекта Kotlin
Видео Android Summit 2020
Google устанавливает новый срок получения разрешений для приложений с фоновой геолокацией
Modern Android Development (MAD) Skills: обучение современной Android-разработке от Google
Oversecured запускается на самофинансировании в размере 1 миллиона долларов благодаря выплатам за найденные ошибки
Прагматичное руководство по Hilt с Kotlin
Обнаружение скриншотов в Android
Уменьшите использование памяти вашим Android-приложением вдвое
Что крутится, то крутится
Создаем приложение с помощью Dropwizard, Maven и Kotlin
Server-Driven UI для Android с Jetpack Compose
Используем Android Jetpack KTX
Новый способ обработки состояний и событий в Android с Sealed Classes
Модуляризация приложений Android быстрый взгляд
(Почти) полностью кроссплатформенные приложения на Kotlin
5 новых платформ автоматизации тестирования для Android (2020)
Lottie для Jetpack Compose
Анимация Android RecyclerView на Kotlin
Kotlin Flow в Android обрабатываем асинхронный поток
Полное руководство по интернационализации и локализации Android-приложений
BasicAirData GPS Logger: открытый GPS-трекер для Android
Currency Converter: конвертер валют для Android

Разработка

(+25) API для QA: тестируем фичи без доступа к коду
(+18) Опыт выбора кроссплатформенной технологии для разработки приложения. Доклад Яндекса
(+5) ECSвUIвклиентеWorld of Tanks Blitz
(+4) Разработка продукта: в какой парадигме работать?
(+4) Материальный дизайн. Создание анимаций в Kivy
(+3) Переходим В OFFLINE FIRST с использованием Core Data и Managed Document(s)
(+3) Тестирование игр
(+1) Flutter. Слушатель клавиатуры без платформенного кода
(+1) Figma выкатила новый Auto Layout
(0) Виды биометрии в мобильном приложении
Podlodka #190: обязательные знания для программиста
Что делать, если вас копирует Apple
MY.GAMES и Google запустили программу по поддержке игровых разработчиков
Что такое Server-Driven UI
Какие языки хотят изучать разработчики?
7 советов для создания лучшего UX: лучшие практики мобильного дизайна
Первый онлайн-хакатон по Flutter в России Liga Stavok Flutterthon
Навигация в mcommerce-приложениях: шаблоны и подводные камни
Прекратите оценивать продуктовых дизайнеров, как визуальных дизайнеров
Вот 20 советов по программированию, о которых вы не просили
4 признания после 4 лет работы разработчиком-самоучкой
Что я узнал о UX, попивая чай
Переопределяем приложение для знакомств пример UX/UI
6 месяцев маленьких проектов
Я недостаточно умен, чтобы быть программистом
Чистая архитектура с точки зрения технического интервью
Simplenote: кроссплатформенный блокнот

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

(+43) В тюрьму за приложение
(+3) Привлечение пользователей, улучшая игровой опыт и ROI
Приложения с виджетами получили более 45 млн. установок на iOS
make sense podcast: О формулировании гипотез, подходах и критериях корректности, и проверке гипотез разных уровней
Руководство по росту приложений для независимых разработчиков
Лучшие мобильные игры в The Game Awards 2020
Global App Testing исследовал привычки пользователей в работе с приложениями
Getaround: рост p2p каршеринга
Как получить больше трафика из рекомендаций похожих приложений?
Сравнительный анализ ценовой стратегии более 100 мобильных приложений на основе подписки
Навыки и требования к аналитикам данных на разных уровнях в Яндексе

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

(+19) Глубокие нейросети в компьютерном зрении: как работают, где используются и какие возникают проблемы
Урок цифры научит школьников работать с нейронными сетями
Ride Vision с помощью ИИ улучшает безопасность мотоциклистов
Huawei полностью продает свой бренд по производству смартфонов Honor

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

Ловец идей, почти хакатон, или как быстро прокачать команду в DartFlutter

25.11.2020 08:15:25 | Автор: admin
image

Итак, дано: команда чуть больше 50человек. Восновном занимаемся web-разработкой совсякими интеграциями, личными кабинетами иe-commerce. Однако впоследнее время около 20% это мобильная разработка. Есть еще отдельная компактная команда, пишущая наDart/Flutter.

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

Основной мобильный стек, который мыиспользовали раньше React Native иFlutter. Нам важна портабельность, это покупают. Оба показали себя очень достойно всвоём классе для разработки бизнес-приложений. Есть известные ограничения инарекания разработчиков, ноэто тема отдельной статьи (Есть всего два типа языков программирования: те, накоторые люди всё время ругаются, ите, которые никто неиспользует Bjarne Stroustrup). Например, приложения наFlutter получаются более сексуальными ишустрыми, нокак дело доходит довиджетов приходится писать нанативе. Реактовиков унас исторически больше (пересесть свебовского React вReact Native довольно просто), авот Dart/Flutter-разработчик зверь редкий.

Унас уже был опыт разработки прикольных проектов врамках хакатонов (бесплатный HelpDesk, Хуижин, PlanningPoker иеще пара подвигов). Нопрактика показала, что врамках хакатона брать незнакомую технологию дело гиблое. Половину времени будут IDE настраивать. Ярешил сделать внутренний конкурс сденьгами ипризами запервые три места. Судить будет тасамая опытная-компактная команда, которая наFlutter уже собаку съела. Осталось выбрать несильно сложный проект, желательно, чтобы внем был еще какой-то практический смысл. Скажу сразу, невсе прошло гладко, сказалась неопытность втаких делах.

Мырешили реинкарнировать Squarespace Note, которое водин момент перестал поддерживать его разработчик. Это ловец идей, который тыоткрываешь, пишешь себе заметку иотправляешь всвой тудушник или электропочту. Завернули затею вовнутренний конкурс среди программистов Сибирикс. Назвали проект SingularityNote. Дали дизайн, бэклог, стэк технологий (Dart+Flutter или React Native) ивсе конкурсные дела: правила, жюри ипризовой фонд.

Что пишем


За3новогодние недели (конкурс проходил всамом начале 2020года) нужно сделать минималистичный редактор заметок свозможностью отправить заметку вSingularity App, напочту или Evernote, приложение должно быть нарусском ианглийском языках взависимости отлокации пользователя.
Фичи конкурсного приложения:

  1. заметку можно отправить всвой Singularity App, Evernote или напочту (+ настройка ихпривязки, + стейты иконок взависимости отнастроек отправки),
  2. кзаметке можно прикрепить изображение изгалереи устройства или снимок камеры,
  3. функций минимум. Все манипуляции сзаметками одним свайпом:
    • Отправить готовую заметку? Свайп вверх.
    • Удалить? Свайп вниз.
    • Посмотреть все отправленные заметки? Свайп вправо.
    • Изменить настройки? Свайп влево.
    • Исмена темы при встряхивании телефоном.
  4. язык интерфейса ру/en взависимости отлокации пользователя,
  5. для каждой отправленной заметки всписке видно: начало текста, дата создания, куда она отправлена,
  6. если заметку неудалось отправить (например, интернета нет), она становится серой, апри тапе нанее повторная отправка,
  7. рекламный баннер нанаш смежный проект.


image

Стек технологий икритерии оценки


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

  1. React Native (React + Redux (предпочтительно)/ Mobx (норм паровоз).
  2. Dart + Flutter (должен использоваться паттерн BLoc).

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

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

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

image

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

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

Как это было


Решились участвовать четыре команды разработчиков. Старт 4января. Попути одна команда слилась. Остальные 27января отдают работы жюри наоценку. Оценивали подвум параметрам:

  1. Культурный код. Вкоде недолжно было быть дичи, иего можно легко поддерживать (от1до10баллов),
  2. Юзабилити. Качество реализации. Стабильность приложения (от1до10баллов).

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

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

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

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

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

P.S. Участие вконкурсе потом сильно мне пригодилось вразработке мобильного приложения для ПроАптека.

Евгений
2 место

Явыбрал React Native, потому что мне нравитсяJS исним яболее-менее знаком, ноконкретно наReact Native + Redux писал впервые. Походу изучал, как всё работает поразным статьям ивидео-урокам. НаJSписал только яиобойти нанём крутых кодеров наDart уменя небыло шансов. Ядаже был единственным, кто сделал авторизацию через QR-код, ноиэтого нехватило, чтобы победить дартеров :)

Денис
1 место

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

Сделал приложение примерно за4-6 восьмичасовых дней (суммарно). Первые дни разработки проходили под температурой 38-39. Видимо, это скорее помогло, чем помешало :)

Что потом


Апотом мыдоработалито, что нужно было доработать, иотправили приложение вофициальные сторы Google Play иApp Store.
Денис
1 место

Боевую версию для сторов допиливал тожея, уже врабочее время. Моя конкурсная версия приложения работала только наAndroid, потому что дома небыло ничего наiOS. Теперь нужна была иверсия для iOS сделалее, еще дополнительный бэкенд для отправки писем исоздания заметок вEvernote (такая опция была доступна только для PRO-аккаунта Evernote), сканер QR-кодов иеще мелкие доработки.

НаSummer Tagline Awards вкатегории Лучшее мобильное MVP-приложение проект взял бронзу.

image

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

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

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


Что неполучилось:

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

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

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

29.11.2020 16:21:40 | Автор: admin
В этом выпуске сравнение карт iOS 1 и 14, паяльник для разработчиков, базовые концепции и базовые ошибки, продуктовые исследования и тренды рынка мобильных приложений в итоговом отчете за 2020 год. Подключайтесь!



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

iOS

(+10) Как добавить 3D в iOS приложение c помощью SceneKit
(+7) SwiftUI 2.0: будущее декларативно
(+4) Интеграция CI/CD для нескольких сред с Jenkins и Fastlane. Часть 3
98% приложений в App Store зарабатывает 8% всех денег
Сравнение iPhone OS 1.0 с iOS 14 с помощью карт
Каникулы App Store: с 24 по 27 декабря
Следует ли мне использовать SwiftUI в проде? Примеры кода, который поможет вам принять решение
Сборка и запуск Telegram-iOS в симуляторе Xcode 12.x
WebSockets и Swift: инженерные задачи на стороне клиента
Имитация плохой сети в iOS
До свидания, AppDelegate! Жизненный цикл приложения SwiftUI
Привет (новый) мир! Жизнь и разработка на Apple Silicon M1
Почему хорошему Swift-разработчику не нужно знать множество шаблонов
SwiftUI-Animations: библиотека анимаций
HHCustomCorner: скругление углов

Android

(+49) Я месяц провел в MIT и понял даже софтверным инженерам не стоит забывать про паяльник
(+20) TV Box или Smart TV?
(+10) Корутинная эволюция в Kotlin. Чем отличаются Channels, Broadcast channels, Shared flows, State flows
(+10) Магическая шаблонизация для Android-проектов
(+9) Kotlin FP: моноиды и сортировки
(+9) Избегаем поддельных шрифтов в Android
(+6) Koin библиотека для внедрения зависимостей, написанная на чистом Kotlin
(+6) Валидация элементов формы textInputLayout в Android с помощью связывания данных
(+4) Как сделать цветные тени в Android с градиентом и анимацией
(+1) Тестируем Android-приложение правильно
Android Broadcast: разработка под устройства
Основы Jetpack Compose: Урок 1
Android Runtime (ART) станет Mainline-модулем в Android 12
Android сможет быстрее обновлять эмодзи
Jetpack Compose: ViewModels
Обработчики эффектов Jetpack Compose
Практическая работа с сетью для разработчиков Android
Простая разработка под Android с использованием Simple-Stack
Android Vitals профилирование запуска приложения
Сегментация изображений в Android с Fritz AI
Kotlin на 60FPS: Kotlin медленный?
Переход с Dagger на Hilt стоит ли?
Android Architecture Components сделайте приложение своей мечты!
Простая кастомная клавиатура с InputConnection
NoNameBottomBar: нижнее меню для Android
My Memory: игра на запоминание на Kotlin

Разработка

(+28) Как я стал разработчиком игр для мобильных телефонов
(+17) Как и почему мы стали делать UI в Figma вместо Photoshop
(+16) Снятся ли телефонам с HMS электроовцы? Обзор функций и возможностей инструментов AI от Huawei
(+15) Конечные автоматы на страже порядка
(+11) Базовые концепции Unity для программистов
(+10) Math Invasion. Мой долгострой
(+8) Расширяемая и удобная в сопровождении архитектура игр на Unity
(+7) Разработка мобильных приложений на Python. Создание анимаций в Kivy. Part 2
(+7) Тестирование игр
(+5) Как устроена система тестирования платежного ядра мобильного приложения
(+3) MMORPG больше не в Telegram Swift и Kotlin Первый большой проект Часть 1
(+2) Качественное тестирование ПО
(+1) Автоматизация тестирования приложений Salesforce
15 ошибок в программировании, которые совершал каждый разработчик
Дизайн приложений: примеры для вдохновения #23
Что такое Разработка через тестирование (Test Driven Development)
Рабочий день геймдизайнера
Итоги конкурса Games Cup 2020
Зачем нужно знать всякие низкоуровневые вещи
UX и Точка. Как устроены продуктовые исследования
Разработка крупных приложений на Xamarin: в чем выгода
6 типов проектов, которые должен попробовать каждый разработчик
Полный курс Firebase Authentication и Flutter 2020
5 улучшений, которых заслуживает App Store исследование UX
Микровзаимодействия: суперсила дизайнеров
Глассморфизм в пользовательских интерфейсах
Flutter vs Kotlin Multiplatform: руководство на 2021 год
Управление идентичностью разработчиков в автономных командах
Flutter может и не станет Next Big Thing, но Kotlin Multiplatform никуда не денется
Масштабирование CI для мобильных инженеров
5 простых советов по написанию чистого кода
Как я стал более ценным программистом за 6 месяцев (и как вы тоже можете)

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

(+27) Внедряем кросс-платформенные пуш-уведомления: дополнительные возможности
(+6) Сравнение аналитических систем для мобильного маркетинга
Тренды рынка мобильных приложений 2020 отчет Liftoff
make sense podcast: О механизмах внешней и внутренней мотивации и их применении в геймификации
LOVEMOBILE #09: NGrow.ai
Хочу в геймдев #16: Локализатор игр
Podlodka #191: маркетинг
Bald Dating дейтинг для лысых
ByteDance запускает магазин мобильных игр и собственное издательство
AudioMob получил $1.5 млн. на аудиорекламу в играх
Xesto: 3D сканирование стоп
Реклама в iOS 14: как получить согласие пользователя руководство Adjust
24 полезных совета для правильного управления репутацией мобильного приложения

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

(+57) Трансформеры в Поиске: как Яндекс применил тяжёлые нейросети для поиска по смыслу
(+6) От хорошей работы не только волки дохнут, но и движки распознавания возникают
Видео PyTorch Virtual Developer Day

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

Категории

Последние комментарии

© 2006-2020, personeltest.ru