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

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

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

23.06.2020 16:19:11 | Автор: admin
Тот, кто занимается машинным обучением (Machine Learning, ML), обычно, реализуя различные проекты, выполняет следующие действия: сбор данных, их очистка, разведочный анализ данных, разработка модели, публикация модели в локальной сети или в интернете. Вот хорошее видео, в котором можно узнать подробности об этом.


Жизненный цикл проекта в сфере машинного обучения

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

Автор статьи, перевод которой мы сегодня публикуем, хочет рассказать о том, как, используя Python-библиотеки streamlit, pandas и scikit-learn, создать простое веб-приложение, в котором применяются технологии машинного обучения. Он говорит, что размер этого приложения не превышает 50 строк. Статья основана на этом видео, которое можно смотреть параллельно с чтением. Инструменты, которые будут здесь рассмотрены, кроме прочего, позволяют ускорить и упростить развёртывание ML-проектов.

Обзор модели, определяющей вид цветка ириса


Сегодня мы создадим простое веб-приложение, использующее технологии машинного обучения. Оно будет классифицировать цветки ириса из выборки Фишера, относя их к одному из четырёх видов: ирис щетинистый (iris setosa), ирис версиколор (iris versicolor), ирис виргинский (iris virginica). Возможно, вы уже видели множество ML-примеров, построенных на основе этого знаменитого набора данных. Но, надеюсь, то, что я тут буду рассматривать ещё один такой пример, вам не помешает. Ведь этот набор он как lorem ipsum классический бессмысленный текст-заполнитель, который вставляют в макеты страниц.

Нам, чтобы построить модель и опубликовать её где-нибудь, понадобятся библиотеки streamlit, pandas и scikit-learn. Взглянем на общую схему проекта. Он будет состоять из двух больших частей: фронтенд и бэкенд.

Во фронтенд-части приложения, а именно, на веб-странице, будет боковая панель, находящаяся слева, в которой можно будет вводить входные параметры модели, которые связаны с характеристиками цветков ириса: длина лепестка (petal length), ширина лепестка (petal width), длина чашелистика (sepal length), ширина чашелистика (sepal width). Эти данные будут передаваться бэкенду, где предварительно обученная модель будет классифицировать цветки, используя заданные характеристики. Фактически, речь идёт о функции, которая, получая характеристики цветка, возвращает его вид. Результаты классификации отправляются фронтенду.

В бэкенд-части приложения то, что ввёл пользователей, сохраняется в датафрейме, который будет использоваться в виде тестовых данных для модели. Потом будет построена модель для обработки данных. В ней будет применяться алгоритм случайный лес из библиотеки scikit-learn. И наконец, модель будет применена для классификации данных, введённых пользователем, то есть для определения вида цветка. Кроме того, вместе со сведениями о виде цветка, будут возвращаться и данные о прогностической вероятности. Это позволит нам определить степень достоверности результатов классификации.

Установка библиотек


Как уже было сказано, здесь мы будем пользоваться тремя библиотеками: streamlit, pandas и scikit-learn. Установить их можно, пользуясь pip install:

pip install streamlitpip install pandaspip install -U scikit-learn

Разработка веб-приложения


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

import streamlit as stimport pandas as pdfrom sklearn import datasetsfrom sklearn.ensemble import RandomForestClassifierst.write("""# Simple Iris Flower Prediction AppThis app predicts the **Iris flower** type!""")st.sidebar.header('User Input Parameters')def user_input_features():sepal_length = st.sidebar.slider('Sepal length', 4.3, 7.9, 5.4)sepal_width = st.sidebar.slider('Sepal width', 2.0, 4.4, 3.4)petal_length = st.sidebar.slider('Petal length', 1.0, 6.9, 1.3)petal_width = st.sidebar.slider('Petal width', 0.1, 2.5, 0.2)data = {'sepal_length': sepal_length,'sepal_width': sepal_width,'petal_length': petal_length,'petal_width': petal_width}features = pd.DataFrame(data, index=[0])return featuresdf = user_input_features()st.subheader('User Input parameters')st.write(df)iris = datasets.load_iris()X = iris.dataY = iris.targetclf = RandomForestClassifier()clf.fit(X, Y)prediction = clf.predict(df)prediction_proba = clf.predict_proba(df)st.subheader('Class labels and their corresponding index number')st.write(iris.target_names)st.subheader('Prediction')st.write(iris.target_names[prediction])#st.write(prediction)st.subheader('Prediction Probability')st.write(prediction_proba)

Разбор кода


Теперь разберём этот код.

Импорт библиотек


import streamlit as stimport pandas as pdfrom sklearn import datasetsfrom sklearn.ensemble import RandomForestClassifier

В этих строках мы импортируем библиотеки streamlit и pandas, назначая им, соответственно, псевдонимы st и pd. Мы, кроме того, импортируем пакет datasets из библиотеки scikit-learn (sklearn). Мы воспользуемся этим пакетом ниже, в команде iris = datasets.load_iris(), для загрузки интересующего нас набора данных. И наконец, тут мы импортируем функцию RandomForestClassifier() из пакета sklearn.ensemble.

Формирование боковой панели


st.sidebar.header('User Input Parameters')

В этой строке мы описываем заголовок боковой панели, используя функцию st.sidebar.header(). Обратите внимание на то, что тут sidebar стоит между st и header(), что и даёт полное имя функции st.sidebar.header(). Эта функция сообщает библиотеке streamlit о том, что мы хотим поместить заголовок в боковую панель.

def user_input_features():sepal_length = st.sidebar.slider('Sepal length', 4.3, 7.9, 5.4)sepal_width = st.sidebar.slider('Sepal width', 2.0, 4.4, 3.4)petal_length = st.sidebar.slider('Petal length', 1.0, 6.9, 1.3)petal_width = st.sidebar.slider('Petal width', 0.1, 2.5, 0.2)data = {'sepal_length': sepal_length,'sepal_width': sepal_width,'petal_length': petal_length,'petal_width': petal_width}features = pd.DataFrame(data, index=[0])return features

Здесь мы объявляем функцию user_input_features(), которая берёт данные, введённые пользователем (то есть четыре характеристики цветка, которые вводятся с использованием ползунков), и возвращает результат в виде датафрейма. Стоит отметить, что каждый входной параметр вводится в систему с помощью ползунка. Например, ползунок для ввода длины чашелистика (sepal length) описывается так: st.sidebar.slider(Sepal length, 4.3, 7.9, 5.4). Первый из четырёх входных аргументов этой функции задаёт подпись ползунка, выводимую выше него. Это, в данном случае, текст Sepal length. Два следующих аргумента задают минимальное и максимальное значения, которые можно задавать с помощью ползунка. Последний аргумент задаёт значение, выставляемое на ползунке по умолчанию, при загрузке страницы. Здесь это 5.4.

Создание модели


df = user_input_features()

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

iris = datasets.load_iris()

Загрузка набора данных Iris из пакета sklearn.datasets и запись его в переменную iris.

X = iris.data

Создание переменной Х, содержащей сведения о 4 характеристиках цветка, которые имеются в iris.data.

Y = iris.target

Создание переменной Y, которая содержит сведения о виде цветка. Эти сведения хранятся в iris.target.

clf = RandomForestClassifier()

Здесь мы, пользуясь функцией RandomForestClassifier(), назначаем классификатор, основанный на алгоритме случайный лес, переменной clf.

clf.fit(X, Y)

Тут мы обучаем модель, пользуясь функцией clf.fit(), передавая ей в качестве аргументов переменные X и Y. Суть происходящего заключается в том, что модель будет обучена определению вида цветка (Y) на основе его характеристик (X).

prediction = clf.predict(df)

Получение сведений о виде цветка с помощью обученной модели.

prediction_proba = clf.predict_proba(df)

Получение сведений о прогностической вероятности.

Формирование основной панели


st.write("""# Simple Iris Flower Prediction AppThis app predicts the **Iris flower** type!""")

Здесь мы, пользуясь функцией st.write(), выводим текст. А именно, речь идёт о заголовке, выводимом в главной панели приложения, текст которого задан в формате Markdown. Символ # используется для указания того, что текст является заголовком. За строкой заголовка идёт строка обычного текста.

st.subheader('User Input parameters')

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

st.write(df)

Этой командой мы выводим на основную панель содержимое датафрейма df.

st.subheader('Class labels and their corresponding index number')

Данный код описывает второй подзаголовок основной панели. В этом разделе будут выведены данные о видах цветков.

st.write(iris.target_names)

Здесь, во второй раздел основной панели, выводятся названия видов цветков (setosa, versicolor и virginica) и соответствующие им номера (0, 1, 2).

st.subheader('Prediction')

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

st.write(iris.target_names[prediction])

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

st.subheader('Prediction Probability')

Выводим заголовок четвёртого (и последнего) раздела основной панели. Здесь будут представлены данные о прогностической вероятности.

st.write(prediction_proba)

Вывод данных о прогностической вероятности.

Запуск веб-приложения


Код приложения сохранён в файле iris-ml-app.py. Мы готовы к тому, чтобы его запустить. Сделать это можно, выполнив следующую команду в терминале:

streamlit run iris-ml-app.py

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

> streamlit run iris-ml-app.pyYou can now view your Streamlit app in your browser.Local URL: http://localhost:8501Network URL: http://10.0.0.11:8501

Через несколько секунд должно появиться окно браузера, в котором будет открыт адрес http://localhost:8501.

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


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

Итоги


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

Пользуетесь ли вы библиотекой streamlit?



Подробнее..

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

21.06.2020 16:16:20 | Автор: admin
Эй, Хаброжитель, приветствую тебя! Буду признателен ко всем твоим фидбэкам.

image

Небольшая предыстория


В 2019 году увлёкся iOS-разработкой и решил попробовать попасть на курс от Mail.ru в их Технопроект с нашим ВУЗом. Закончил данный курс с отличием. Огромное спасибо Диме и Гена за отличный курс. После этого курса начал активно посещать митапы iOS-разработчиков. Летом особо не прогал под iOS. Осенью все же надумал найти работу в этой сфере и развиваться дальше. Решил начать с небольших компаний, поэтому нагуглил топ-100 аутсорс компаний по разработке приложений. Написал всем компаниям, которые находились в Москве.

Из приблизительно 70 компаний ответили около 15 и где-то 3-4 пригласили на интервью.

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

В это время нашел курс по iOS-разработке от Яндекса и прошел его.

Собеседования в Яндексе


В начале декабря был на митапе в Яндексе для джунов. Всем гостям раздали QR-коды для прохождения первого алгоритмического собеса. Там было 3 задачи на 2 часа без перерыва.

Задачи:

  1. Вводится n и нужно посчитать сумму всех чисел от 1 до n < 1000000, где цифры не встречаются более одного раза.
  2. Есть доска M x N (2<M,N<1000000) есть белая и черная лошадь. На входе начальная точка черной и белой лошади. Нужно совершить минимальное кол-во ходов, чтобы они оказались в одной точке и вывести это кол-во.
  3. Вводиться 2 числа N-кол-во чисел в массиве и M-сумма подмассива. Нужно найти подмассив с минимальным кол-во чисел, сумма которой равнялась M, в противно случае нужно вывести что-то другое (не помню).

Есть ограничения по времени и по памяти.

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

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

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

  1. Есть пять человек: Т, М, А, Л, Д. Они стоят в очереди за магической колой, т.е. тот, кто выпивает его удваивается. На входе n номер следующего, кто должен выпить колу. Нужно найти того, кто будет n-ным.

    Идея решения
    Лучшее решение это hashmap, сам решил через арифметику, т.е. вывел формулу, которая посчитала бы это. //Рассказал всю идею того, как решал бы, потом дал оценку по времени и по памяти, а когда начал писать код, то интервьюер сказал, что он все понял и что код я ему точно напишу так, что давай перейдём к следущей задаче, т.к. у нас осталось чуть больше 20 минут
  2. Есть бинарное дерево поиска. Нужно найти сумму элементов лежащих в сегменте от L до R. Функция на вход получает корневой элемент, левую и правую границу. Тогда уточнил и узнал, что элементы не повторяются и границы включены.

    Идея решения
    Делал через рекурсию. (можно через цикл, но рекурсия проще), опять же сложность алгоритма и т.д.

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

Пришел на собес, его проводил Слава из Браузера. Немного пообщались об алгоритмической секции. Дальше он попросил написать схематично код, который реализовывал бы работу многопоточности. Так как до это особо не приходилось работать с многопоточностью, то за счет неожиданности попал в ступор минут на 20-30, просто не думал, что придется писать код. По теории отвечал не плохо, но были проблемы с реализацией. При всем при этом собес очень понравился. Мы не успели реализовать то, что он просил, поэтому Слава предложил дорешать, и нам пришлось сделать это в коридоре, потом немного пообщались. Это было суперски, потому что некоторые ребята ориентируются строго по времени. Через недели две написали, что провалил собес. Было очень обидно т.к. готовился и очень-очень сильно хотел попасть.

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

Собеседование в МегаФоне


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

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

Самоизоляция говорит: Нет очным собеседованиям!

image

Собеседование в Сбербанк


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

Собеседование в ВТБ


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

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

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

Заключение


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

Советы, которые могут помочь при поиске первой работы в IT


  1. Нужно хорошо разбираться в теории, ибо научиться кодить не сложно.
  2. Откликайтесь на вакансии middle/senior, т.к. найти вакансию intern/junior почти невозможно найти.
  3. Если есть возможность попасть на собеседование, то идите, потому что это поможет вам поднять ваши скилы. (Даже если мало что знаете)
  4. Изучите компанию, в которой будете проходить собеседование и покажите интервьюеру свою заинтересованность работать у них.
  5. Обязательно спрашивайте о том, что вас лично интересует и что хотели бы узнать о рабочем процессе.
  6. Когда чего-то не знаете или не помните, то задавайте уточняющие вопросы, они вам помогут. Если не смогли вспомнить, то просто скажите об это.
  7. Думайте открыто, ведь интервьюер больше смотрит на то, как вы думаете и как ищете выход при сложившихся ситуациях. Если вы просто скажете ответ, то это ни о чем не говорит. Когда вы открыто рассуждаете, то интервьюер видит где вы ошиблись и помогает вам, задавая наводящие вопросы.
  8. Прежде чем пойти на собеседование отрепетируйте его. Задавайте себе всякие вопросы связанные с тем, что должно быть и постарайтесь внятно ответить на них.

Совет компаниям


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

Если будут желающие, то сделаю еще пост с самыми популярными и важными вопросами + ответы и материалы к ним.
Подробнее..

Безопасность мобильных устройств и приложений пять популярных сценариев атак и способы защиты

06.07.2020 16:22:04 | Автор: admin


Изображение: Unsplash

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

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

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



Существует пять основных сценариев атаки. Среди них:

  • Физический доступ. Если телефон был украден или потерян, владелец отдал его в сервис или подключил к поддельному зарядному устройству по USB все это открывает возможность для атаки.
  • Вредоносное приложение на устройстве. Иногда такие приложения могут попасть на устройство даже из официальных источников, Google Play и App Store (для Android, для iOS).
  • Атакующий в канале связи. Подключившись к недоверенному Wi-Fi, прокси-серверу или VPN, мы становимся уязвимыми для атак в канале связи.
  • Удаленные атаки. Атакующий может действовать при этом удаленно, пользуясь серверами мобильных приложений или иными службами для доставки эксплойта.
  • Атаки на серверную часть. Отдельно от всего можно рассмотреть атаки на серверную часть мобильных приложений, поскольку в этом случае доступ к устройству злоумышленнику не требуется.

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

Атаки с физическим доступом


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

Зарядная станция, к которой вы подключаете свой смартфон по USB, вполне может оказаться не совсем безопасной. Для современных версий ОС Android и iOS при подключении с смартфона к ПК по USB требуется разрешение на доступ к устройству. Однако на Android 4.0 и ниже этого не требовалось. В итоге при подключении таких устройств к скомпрометированным или установленным хакерами зарядным станциям, открывается возможность для атаки. Ее сценарий может выглядеть так:

  • На вашем смартфоне с версией Android 4.0 или ниже доступна отладка по USB.
  • Вы подключаетесь к зарядной станции по USB-кабелю.
  • Вредоносная зарядная станция выполняет команду adb install malware.apk, чтобы установить вредонос на ваше устройство.
  • Вредоносная зарядная станция выполняет команду adb am start com.malware.app/.MainActivity для запуска этого вредоносного приложения.
  • Запущенный троян пробует различные техники повышения привилегий, получает права root и закрепляется в системе. Теперь ему доступны все хранимые данные, включая аутентификационные (логины, пароли, токены) от всех установленных приложений, а также неограниченный доступ к любому приложению во время исполнения.

Как защититься


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

Атаки с помощью вредоносных приложений


Есть несколько источников таких приложений:

  • Официальные магазины приложений Google Play и App Store. Редко, но даже в официальных маркетах можно найти вредоносное приложение, которое может нанести ущерб вам и вашим данным. Часто такие приложения стараются заполучить побольше установок с помощью кликбейтных названий типа Super Battery, Turbo Browser или Virus Cleaner 2019.
  • Неофициальные сайты и магазины приложений (third-party appstore). Для Android-устройств достаточно разрешить установку из недоверенных источников, а затем скачать apk-файл приложения с сайта. Для iOS-устройств достаточно перейти по ссылке в браузере Safari, подтвердить установку сертификата на устройство, после чего любое приложение в этом неофициальном магазине станет доступно для установки прямо из браузера.
  • Пользователь может установить скачанное из интернета приложение с помощью USB-подключения.
  • Для Android-устройств доступна возможность загрузки части приложения при переходе по ссылке механизм Google Play Instant.

При установке на смартфон в зависимости от полученных разрешений вредоносные приложения будут иметь доступ к некоторым хранимым данным, микрофону, камере, геопозиции, контактам и т. п. Также они получат возможность взаимодействия с другими установленными приложениями через механизмы межпроцессного взаимодействия (IPC/XPC). Если установленные приложения содержат уязвимости, которые можно проэксплуатировать через такое взаимодействие, вредоносное приложение сможет этим воспользоваться. Особенно актуально это для Android-устройств.

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

Как защититься


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

Атаки в канале связи


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

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

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

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

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

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

Как защититься


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

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

Удаленные атаки


Некоторые уязвимости в мобильных приложениях можно проэксплуатировать удаленно, и для этого даже не требуется контролировать передачу данных между приложением и сервером. Многие приложения реализуют функциональность по обработке специальных ссылок, например myapp://. Такие ссылки называются deeplinks, и работают они как на Android, так и на iOS. Переход по такой ссылке в браузере, почтовом приложении или мессенджере может спровоцировать открытие того приложения, которое умеет такие ссылки обрабатывать. Вся ссылка целиком, включая параметры, будет передана приложению-обработчику. Если обработчик ссылки содержит уязвимости, то для их эксплуатации будет достаточно вынудить жертву перейти по вредоносной ссылке.

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

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

Как защититься


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

Атаки на серверную часть


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

Зачастую устройство серверной части мобильного приложения ничем не отличается от веб-приложения. Как правило, устроены серверы мобильных приложений еще проще и часто представляют из себя json- или xml-api, редко работают с HTML-разметкой и JavaScript, как это часто делают веб-сайты.

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

  • недостаточная защита от подбора учетных данных: 24% веб-приложений и 58% серверов мобильных приложений содержат такие уязвимости,
  • ошибки бизнес-логики: 2% веб-приложений и 33% серверов мобильных приложений.

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

Как защититься


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

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

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

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

Камера, мотор, панорама как создаются 3D-фото автомобилей в приложении Авто.ру

30.06.2020 10:05:46 | Автор: admin


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

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

Что было до


Крупные автодилеры начали массово использовать панорамы где-то в 2017 году. Строго говоря, это были не совсем панорамы: вокруг автомобиля строили целые съёмочные павильоны или замкнутые рельсы, по которым толкали камеру, пока она не возвращалась на место старта, на выходе видео, где машина видна со всех сторон. В 2018 мы поддержали этот формат на Авто.ру и наблюдали процесс подготовки: дилеры закупают оборудование ценой в сотни тысяч рублей, возводят вспомогательные приспособления камера, мотор! Красивые фото и видео приятно рассматривать даже если не собираешься приобретать автомобиль. Неудивительно, что частные продавцы стали интересоваться, можно ли повторить такое в домашних условиях.

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

И сделали.

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


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

Итак, вы открыли приложение, нажали кнопку Панорама в форме редактирования объявления, обошли автомобиль с включённой камерой, тщательно следуя всем инструкциям. Что дальше?



Шаг 0. Снятое видео отправляется к нам на сервера: во-первых, зачем экспериментировать, справится ли ваше устройство с не самыми лёгкими расчётами, если можем взять это на себя. Во-вторых, чем больше вычислительных мощностей, тем быстрее сгенерируется панорама. Запускается классический для задач Structure from Motion алгоритм обработки видео, который умеет одновременно делать всё, что потребуется для моделирования: позиционировать камеру в пространстве, понимать, где находился объект, и оценивать его размеры.

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

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

Шаг 2. Для опорных кадров алгоритм ищет соседние: выбирает таким образом, чтобы они равномерно (по углу обхода), покрывали панораму. То есть точки, существующие в реальном 3D-мире, не рассчитываются для каждого нового кадра, а ищутся на соседних.

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

*Мы уже ссылались на эту статью выше: больше деталей, математические подробности.

Шаг 3. Облако точек есть: как понять, какие из них относятся к машине? Технология, помогающая по кадрам создавать облако точек и находить позицию камеры, довольно известна. Мы для решения этой задачи используем opensfm. Однако следом отрабатывает наша собственная технология, позволяющая сделать фильтрацию облака точек и выравнивание позиции камеры. Эвристика следующая:

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

Итого: ищем некий параллелепипедообразный (bbox) объект, в который входит как можно больше точек из облака. Если таких объектов оказалось более одного (в кадр попал столб или что-то ещё), сегментация оставит только центральный.

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

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

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

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

Лучше один раз увидеть:



Шаг 5. От облака точек возвращаемся к понятной человеческому глазу картинке: панораме.

Когда мы саппроксимировали bounding box машины, можно оценить, насколько много именно её попало на разные кадры и отбраковать неудачные. Если известно местоположение объекта, все нужные точки легко спроецировать на каждый кадр, обрезать его до нужных размеров. Параллельно алгоритм восстанавливал параметры камеры: он знает, когда картинку нужно отмасштабировать (приблизить/удалить) или повернуть.

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

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

Шаг 6. Готовая панорама отправляется в объявление.



Немного цифр для масштаба


Сейчас для создания панорамы используется около 60 опорных кадров, приклеиваются к ним ещё около 120. Для этого выбирается по 15 соседних кадров, для них вычисляются позиции камеры и облако совпавших точек. Количества кадров не случайны: так не страдает ни качество панорамы, ни скорость сборки. Первые панорамы, где мы ещё не нащупали баланс, собирались по 40 минут, что, конечно, недопустимо.
Обычно съёмка панорамы автомобиля происходит где-то за минуту. На смартфонах видео такой длительности после съёмки занимает от 100 до 300 МБ, что с условием загрузки на наши сервера было бы серьёзным стопором для пользователя. Поэтому в рамках проекта мы стали на лету менять битрейт и формат съёмки, тем самым сократив размер файла видео до приемлемых 2040 мегабайт, без потерь в размере картинки или качества для компьютерного зрения.
С другой стороны, нам пришлось провести адаптацию выдачи объявлений с панорамами, т.к. пользователи не готовы ждать загрузки 56 мегабайт на каждое, учитывая, что за сессию поиска автомобиля они просматривают десятки и даже сотни объявлений. Для решения этой проблемы мы перекодируем полученный результат под разные разрешения и форматы файлов от простого сета .jpeg до достаточно редкого .webm, в некоторых случаях сокращая размер скачиваемой панорамы до 150 КБ.
На данный момент у нас на сайте можно встретить больше 3000 панорам автомобилей, созданных как дилерами (мы не забыли и про них), так и обычными продавцами.
Будущие покупатели видят в выдаче живые объявления с панорамами и залипают на них до 30% дольше. Конверсия звонков продавцу для автомобиля с панорамой также растёт.

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

Товар != автомобиль. Надеемся, что наш опыт окажется полезным и для других сфер ритейла: простое, быстрое и бесплатное создание 3D-модели для объявления облегчит жизнь продавцам, возможность тщательно осмотреть предмет сделки поможет покупателям.
Подробнее..

Из песочницы Кодовая база. Расширяем RecyclerView

26.06.2020 18:17:05 | Автор: admin
image
Всем привет!

Меня зовут Антон Князев, senior Android-разработчик компании Omega-R. В течение последних семи лет я профессионально занимаюсь разработкой мобильных приложений и решаю сложные проблемы нативной разработки.

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

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

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

Первая библиотека, которую мы выложили на GitHub, является простым расширением RecyclerView.

Начнем с проблем, которые она решала:

  1. Нет дефолтного layoutManager это неудобно, так как часто приходится выбирать один и тот же LinearLayoutManager;
  2. Нет возможности добавлять divider и item space через xml тоже неудобно, так как необходимо добавлять либо в item layout, либо через ItemDecorator;
  3. Нельзя просто добавить header и footer через xml это возможно только через отдельный ViewHolder.

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

1. Проблема: нет дефолтного layoutManager


Разработчики RecyclerView не предусмотрели возможности выбора LayoutManager по умолчанию. Задать layoutManager можно следующими способами:

1. через XML в атрибуте app:layoutManager=LinearLayoutManager:

<?xml version="1.0" encoding="utf-8"?><androidx.recyclerview.widget.RecyclerView   ...    app:layoutManager="LinearLayoutManager"/>

2. через код:

recyclerView.layoutManager = LinearLayoutManager(this)

По нашему опыту, в большинстве случаев нужен именно LinearLayoutManager.

Вот несколько примеров таких списков из наших приложений ITProTV, Простой Мир и Dexen:

image

Решение: добавим дефолтный layoutManager


В OmegaRecyclerView добавляется лишь 3 строчки:

 if (layoutManager == null)  {            layoutManager = LinearLayoutManager(context, attrs, defStyleAttr, 0) }

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

<?xml version="1.0" encoding="utf-8"?><com.omega_r.libs.omegarecyclerview.OmegaRecyclerView    android:id="@+id/recyclerview"    android:layout_width="match_parent"    android:layout_height="match_parent" />

2. Проблема: нет возможности добавлять divider и item space через xml


Добавлять divider приходится довольно часто при использовании RecyclerView. Например, в проекте Простой Мир один из экранов был с таким нестандартным divider:

image

Из этого макета видно, что:

  • используются divider между элементами и в самом конце;
  • используется item space.

Каким образом это можно реализовать в Android стандартным путем?

Способ 1


Самый очевидный способ включить divider как элемент ImageView:

 <RelativeLayout   ...   android:paddingStart="20dp"   android:paddingTop="12dp"   android:paddingEnd="20dp"   android:paddingBottom="12dp">...   <ImageView       ...       android:layout_alignParentBottom="true"       android:src="@drawable/divider"/></RelativeLayout>

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

Способ 2


Другим способом является использование DividerItemDecoration, который может нарисовать этот divider. Для него необходимо дополнительно создать drawable:

<?xml version="1.0" encoding="utf-8"?><layer-list xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android">    <item android:left="32dp">        <shape android:shape="rectangle">            <size                    android:width="1dp"                    android:height="1dp" />            <solid android:color="@color/gray_dark" />        </shape>    </item></layer-list>

Для добавления отступа требуется написать свой ItemDecoration:

class VerticalSpaceItemDecoration(private val verticalSpaceHeight: Int): RecyclerView.ItemDecoration {    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,            state: RecyclerView.State) {        outRect.bottom = verticalSpaceHeight    }}

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

Решение: дополним возможностью добавлять divider и item space через xml


Итак, наш OmegaRecyclerView должен уметь добавлять divider с помощью следующих атрибутов:

  1. divider определяет drawable, может быть назначен и цвет напрямую;
  2. dividerShow (beginning, middle, end) флаги, которые определяют, где рисовать;
  3. dividerHeight задает высоту divider, в случае с цветом становится особенно нужным;
  4. dividerPadding, dividerPaddingStart, dividerPaddingEnd отступы: общий, с начала, с конца;
  5. dividerAlpha определяет прозрачность;
  6. itemSpace отступы между элементами списка.

Все эти атрибуты применяются непосредственно к нашему OmegaRecyclerView с помощью двух специальных ItemDecoration.

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

Для того чтобы упростить код, выделим специальный DividerDecorationHelper, который будет читать и записывать относительные данные в Rect, зависящие от ориентации и порядка следования (обратный или прямой). В нем будут методы setStart, setEnd и getStart, getEnd.

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

  1. проверка, что layoutManager является наследником LinearLayoutManager;
  2. вычисление текущей ориентации и порядка следования;
  3. определение подходящего DividerDecorationHelper.

В SpaceItemDecoration переопределим только один метод getItemOffset, который будет добавлять отступы:

override fun getItemOffset(outRect: Rect, parent: RecyclerView, helper: DividerDecorationHelper, position: Int, itemCount: Int) {        if (isShowBeginDivider() || countBeginEndPositions <= position) helper.setStart(outRect, space)        if (isShowEndDivider() && position == itemCount - countBeginEndPositions) helper.setEnd(outRect, space)    }

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

    override fun getItemOffset(outRect: Rect, parent: RecyclerView, helper: DividerDecorationHelper, position: Int, itemCount: Int) {        if (position == 0 && isShowBeginDivider()) {            helper.setStart(outRect, dviderSize)        }        if (position != 0 && isShowMiddleDivider()) {            helper.setStart(outRect, dividerSize)        }        if (position == itemCount - 1 && isShowEndDivider()) {            helper.setEnd(outRect, dividerSize)        }    }

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

   open fun isDividerAllowedAbove(position: Int): Boolean {        return true    }    open fun isDividerAllowedBelow(position: Int): Boolean {        return true    }

Далее переопределим метод onDrawOver, чтобы рисовать divider поверх нарисованных элементов. В этом методе надо пройтись по всем элементам, видимым на экране (через getChildAt), и при необходимости нарисовать этот divider. Также надо учесть, что из атрибута dividerDrawable может прийти и цвет, у которого нет высоты. Для такого случая высоту можно взять из атрибута dividerHeight.

3. Проблема: нельзя напрямую добавить header и footer через xml


image

В RecyclerView невозможно добавлять view через xml, но есть другие способы сделать это.

Способ 1


Один из очевидных способ добавлении view через adapter. Причем необходимо отличать header и footer в adapter при введении своего идентификатора для viewType.

 fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder? {  val inflater = LayoutInflater.from(parent.context)        return when (viewType) { TYPE_HEADER -> {                val headerView: View = inflater.inflate(R.layout.item_header, parent, false)                HeaderViewHolder(itemView)            }            TYPE_ITEM -> {                val itemView: View = inflater.inflate(R.layout.item_view, parent, false)                ItemViewHolder(itemView)            }                       else -> null        }    }

Способ 2


Немного другой способ, но тоже через adapter. Начиная с recyclerview:1.2.0-alpha02, появился MergeAdapter, который позволяет соединять несколько адаптеров в один, делая код чище.

val mergeAdapter = MergeAdapter(headerAdapter, itemAdapter,  footerAdapter)recyclerView.adapter = mergeAdapter

Решение: дополним возможностью простого добавления header и footer через xml


Первое, что нужно сделать перехватить добавление view в нашем OmegaRecyclerView, когда идет процесс inflate. Для этого следует переопределить метод addView и добавить себе все header и footer view. Этот метод используется самим RecyclerView для дополнения видимыми элементами списка. Но view, добавленные через xml, не будут иметь ViewHolder, что в конечном итоге вызовет NullPointerException.

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

  protected override fun onFinishInflate() {        super.onFinishInflate()        finishedInflate = true    }

Таким образом, метод addView будет выглядеть следующим образом:

  override fun addView(view: View, index: Int, params: ViewGroup.LayoutParams) {        if (finishedInflate) {            super.addView(view, index, params)        } else {            // save header and footer views        }    }

Далее необходимо запомнить все эти добавочные view и передать в специальный адаптер по типу MergeAdapter.

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

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

  1. Мы уже в 2018 году создали свой ViewPager из RecyclerView. Причем, в нашем ViewPager есть бесконечный скролл;
  2. ExpandableRecyclerView специальный класс для добавления раскрывающего списка, с возможностью выбора анимации раскрытия;
  3. StickyHeader специфический элемент списка, который можно добавлять через адаптер.

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

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

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

06.07.2020 10:05:56 | Автор: admin


Flutter предлагает различные виджеты для работы с определенным набором фигур, например, ClipRect, ClipRRect, ClipOval. Но также есть ClipPath, с помощью которого мы можем создавать любые типы фигур.


В данной статье мы сосредоточимся на том, что можно сделать, используя ClipPath и CustomClipper. Поехали!


Содержание:



ClipPath


Благодаря ClipPath мы можем создавать очень сложные и необычные фигуры. В этом нам поможет свойство clipper у ClipPath


@overrideWidget build(BuildContext context) {  return Scaffold(    backgroundColor: Colors.grey,    body: Center(      child: ClipPath(        clipper: MyCustomClipper(), // <--        child: Container(          width: 200,          height: 200,          color: Colors.pink,        ),      ),    ),  );}

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


class MyCustomClipper extends CustomClipper<Path> {  @override  Path getClip(Size size) {    Path path = Path();    return path;  }  @override  bool shouldReclip(CustomClipper<Path> oldClipper) => false}

Метод getClip вызывается каждый раз, когда требуется обновить нашу описанную фигуру. В качестве параметра метод получает Size, который содержит значения высоты и ширины виджета, переданного в сhild у ClipPath.


shouldReclip вызывается, когда в clipper передается новый экземпляр класса. Если новый экземпляр отличается от старого, то метод должен возвращать true, в противном случае false.


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



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


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


lineTo


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



Как показано выше на рисунке (a), путь по умолчанию начинается с точки p1(0, 0). Теперь добавим новый отрезок к p2(0, h), а затем p3(w, h). Нам не нужно определять линию от конечной точки p3 до начальной p1, она будет нарисована по умолчанию.


Результат можно увидеть на рисунке (b) с треугольником розового цвета.


@overridePath getClip(Size size) {  Path path = Path()    ..lineTo(0, size.height) // Добавить отрезок p1p2    ..lineTo(size.width, size.height) // Добавить отрезок p2p3    ..close();  return path;}

moveTo


Этот метод нужен для перемещения точки отрисовки.



Как показано на рисунке выше, начальная точка перемещена из (0, 0) в точку p1(w/2, 0).


@overridePath getClip(Size size) {  Path path = Path() // Начальная точка в (0, 0)    ..moveTo(size.width/2, 0) // передвигаем точку в (width/2, 0)    ..lineTo(0, size.width)    ..lineTo(size.width, size.height)    ..close();  return path;}

quadraticBezierTo


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



Источник: Wikipedia


Как показано на приведенном выше рисунке, мы можем нарисовать квадратичную кривую Безье, используя контрольную и конечную точки. P0 это начальная точка, P1 контрольная точка, а P2 конечная точка.



Как показано выше на рисунке (а), кривая рисуется от точки p2(0, h) до p3(w, h) с использованием контрольной точки c(w/2, h/2).


@overridePath getClip(Size size) {  // Эта переменная определена для лучшего понимания, какое значение указать в методе quadraticBezierTo  var controlPoint = Offset(size.width / 2, size.height / 2);  var endPoint = Offset(size.width, size.height);  Path path = Path()    ..moveTo(size.width / 2, 0)    ..lineTo(0, size.height)    ..quadraticBezierTo(        controlPoint.dx, controlPoint.dy, endPoint.dx, endPoint.dy)    ..close();  return path;}

cubicTo


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



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



Как показано на рисунке (a), кубическая кривая рисуется между начальной точкой p2 и конечной точкой p3 с использованием контрольных точек c1 и c2.


@overridePath getClip(Size size) {  var controlPoint1 = Offset(50, size.height - 100);  var controlPoint2 = Offset(size.width - 50, size.height);  var endPoint = Offset(size.width, size.height - 50);  Path path = Path()    ..moveTo(size.width / 2, 0)    ..lineTo(0, size.height - 50)    ..cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,        controlPoint2.dy, endPoint.dx, endPoint.dy)    ..close();  return path;}

arcToPoint


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



Существуют эллиптический и круговой типы радиуса для построения дуги. Как показано на приведенном выше рисунке, эллиптический радиус рисуется с использованием значения (x, y), а круговой радиус радиуса R.



Как показано на рисунке (a) выше, построение фигуры начинается с точки p1. Первая дуга рисуется от точки p2 до точки p3, при этом радиус не задан, поэтому по умолчанию равен нулю, соответственно наша дуна выглядит, как прямая. Вторая дуга тянется от начальной точки p4 до конечной точки p5 с использованием кругового радиуса и направления по часовой стрелке (прим. по часовой направление по умолчанию). Третья дуга тянется от точки p6 до точки p7, используя круговой радиус и направление против часовой стрелки. Четвертая дуга проходит от начальной точки p8 до конечной точки p1, используя эллиптический радиус.


@overridePath getClip(Size size) {  double radius = 20;  Path path = Path()      ..moveTo(radius, 0)    ..lineTo(size.width-radius, 0)    ..arcToPoint(Offset(size.width, radius))    ..lineTo(size.width, size.height - radius)    ..arcToPoint(Offset(size.width - radius, size.height),radius: Radius.circular(radius))    ..lineTo(radius, size.height)    ..arcToPoint(Offset(0, size.height - radius), radius: Radius.circular(radius), clockwise: false)    ..lineTo(0, radius)    ..arcToPoint(Offset(radius, 0), radius: Radius.elliptical(40, 20))    ..close();  return path;}

arcTo


Этот метод используется, чтобы нарисовать дугу, задав в качестве значения в радианах Rect, начальный угол (startAngle) и конечный угол (sweepAngle).



Приведенное выше изображение предназначено для предоставления основной информации об углах в радианах. Минимальный угол равен 0 PI (значение PI равно ~3.14), полный 2 PI.



Существует несколько способов построения Rect, как с помощью точек, окружности, LTRB (Left, Top, Right, Bottom) и LTWH (Left, Top, Width, Height). На вышеприведенном рисунке (a) все типы дуг нарисованы с разным начальным углом.


@overridePath getClip(Size size) {  double radius = 50;  Path path = Path()    ..lineTo(size.width - radius, 0)    ..arcTo(        Rect.fromPoints(            Offset(size.width - radius, 0), Offset(size.width, radius)), // Rect        1.5 * pi,   // начальный угол        0.5 * pi,  // конечный угол        true)  // направление по часовой стрелке    ..lineTo(size.width, size.height - radius)    ..arcTo(Rect.fromCircle(center: Offset(size.width - radius, size.height - radius), radius: radius), 0, 0.5 * pi, false)    ..lineTo(radius, size.height)    ..arcTo(Rect.fromLTRB(0, size.height - radius, radius, size.height), 0.5 * pi, 0.5 * pi, false)    ..lineTo(0, radius)    ..arcTo(Rect.fromLTWH(0, 0, 70, 100), 1 * pi, 0.5 * pi, false)    ..close();  return path;}

addRect


Данный метод нужен для построения прямоугольников. Есть несколько различных методов для создания Rect: fromPoints, fromLTWH, fromCircle, fromLTRB и fromCircle.



@overridePath getClip(Size size) {  Path path = Path()    ..addRect(Rect.fromPoints(Offset(0, 0), Offset(60, 60)))    ..addRect(Rect.fromLTWH(0, size.height - 50, 50, 50))    ..addRect(Rect.fromCircle(center: Offset(size.width / 2, size.height / 2), radius: 20))    ..close();  return path;}

addRRect


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



@overridePath getClip(Size size) {    double radius = 10;    Path path = Path()      ..addRRect(RRect.fromLTRBR(0, 0, 60, 60, Radius.circular(radius)))      ..addRRect(RRect.fromRectAndRadius(          Rect.fromLTWH(0, size.height - 50, 50, 50), Radius.circular(radius)))      ..addRRect(RRect.fromRectAndCorners(          Rect.fromCircle(              center: Offset(size.width / 2, size.height / 2), radius: 30          ),          topLeft: Radius.circular(radius)))      ..close();    return path;}

addOval


Данный метод используется для описания овала. Как и для addRect, параметр типа Rect является обязательным.



@overridePath getClip(Size size) {    Path path = Path()      ..addOval(Rect.fromPoints(Offset(0, 0), Offset(60, 60)))      ..addOval(Rect.fromLTWH(0, size.height - 50, 100, 50))      ..addOval(Rect.fromCircle(center: Offset(size.width / 2, size.height / 2), radius: 20))      ..close();    return path;}

addPolygon


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



@overridePath getClip(Size size) {    var points = [      Offset(size.width / 2, 0), // точка p1      Offset(0, size.height / 2), // точка p2      Offset(size.width / 2, size.height), // точка p3      Offset(size.width, size.height / 2) // точка p4    ];     Path path = Path()      ..addPolygon(points, false)      ..close();    return path;}

addPath


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



Как показано на рисунке (a), есть две фигуры (path 1 и path 2), path 1 является основной, а path 2 добавляется в path 1. path 2 строится в соответсвии со сдвигом (w/2, 0), поэтому начало координат (0, 0) и все остальные точки вычисляются с учетом указанного смещения.


@overridePath getClip(Size size) {    Path path1 = Path()      ..lineTo(0, size.height)      ..lineTo(size.width/2, size.height)      ..lineTo(0, 0);    Path path2 = Path()      ..lineTo(size.width/2, size.height)      ..lineTo(size.width/2, 0)      ..lineTo(0, 0);    path1.addPath(path2, Offset(size.width/2,0));    return path1;}

relativeLineTo


Этот метод аналогичен методу lineTo, но конечная точка линии задается не точной координатой, а смещением из начальной.



На рисунке (a) линия p1p2 рисуется с помощью relativeLineTo, поэтому координата точки p2 вычисляется относительно p1. Можно записать в виде формулы p2(x, y) = (p1.x + 50, p1.y + h)


@overridePath getClip(Size size) {    Path path = Path()      ..moveTo(size.width/2, 0)      ..relativeLineTo(50, size.height)      ..lineTo(size.width , size.height)      ..close();    return path;}

Примечание: relativeMoveTo, relativeQuadraticBezierTo, relativeArcToPoint, relativeCubicTo будут работать по сравнению с quadraticBezierTo, arcToPoint, cubicTo по тому же принципу, что и relativeLineTo по отношению к lineTo.



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




Подробнее..

Локализация мобильных приложений основные сложности и лайфхаки

22.06.2020 18:15:30 | Автор: admin


Изменения неизбежны. Рост не гарантирован.
Джон Максвелл

Хотя цитата Максвелла и может показаться чрезмерно обобщённой, но она абсолютно применима и к разработке приложения.

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

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

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

Написано в Alconost

1. Изучите рынок приложения как свои пять пальцев


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

Здесь вам помогут два простых вопроса:

Ваше приложение востребовано на местном рынке?

Готовы ли местные пользователи за него платить?

Изучите целевую аудиторию до локализации вашего приложения


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

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

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

Изучите локализации конкурентов в целевом регионе


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

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

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

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


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

2. Обратите внимание на культурные различия


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

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

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

У нас есть яркий пример, как мы справились с культурными различиями проект локализации игры Streets of Rogue. Самым сложным было локализовать диалоги и юмор, характерные для жанра rogue-like, на 7 языков всего за 2 недели пришлось по особому подойти к подбору соответствующих лингвистов для проекта. Однако, по нашему скромному мнению, в итоге всё вышло хорошо, и на проекте было много забавных моментов посмотрите сами!

Английский Французский Немецкий Русский Испанский
How ya doin, fancy pants? La forme, pingouin ? Was geht, Schickimicki? Как дела, фраер? Cmo ests, elegante?
*glug glug* *Glou* *Glou* *gluck gluck* Бульк-бульк * glug glug *
Blood makes you related. Loyalty makes you family. Meatballs make you fart. Le sang cre les liens. La loyaut fonde la famille. Les boulettes de viande provoquent les pets Durch Blut wird man verwandt. Durch Loyalitt wird man Familie. Durch Frikadellen furzt man. Кровь делает нас родственниками. Преданность объединяет нас в семьи. Фрикадельки делают нас пердунами. La sangre te hace pariente. La lealtad te hace familia. Las albndigas te hacen tirar pedos.
HOLY CRAP!!! HOLY HOLY CRAP!!! BON SANG!!! BON SANG DE BONSOIR !!! MEINE GTE!!! MEINE LIEBE GTE!!! БОЖЕЧКИ!!! БО-ЖЕЧ-КИ, БО-ЖЕЧ-КИ!!! SANTO DIOS!!! SANTSIMO DIOS!!!

Примеры перевода некоторых смешных диалогов в игре Streets of Rogue

3. Будьте готовы окунуться в процесс перевода


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

Создание глоссария приложения


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

Актуализация памяти переводов


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

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

Будьте на связи со своей командой по локализации


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

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



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

На схеме ниже мы показали, как примерно выглядит процесс локализации в нашей компании.



Исходя из опыта, мы советуем максимально адаптировать процесс непрерывной локализации (continuous localization) к непрерывной поставке (continuous delivery). Тем самым вы обеспечите непрерывную независимую работу обоих потоков agile-логика. Кроме того, локализация будет тестироваться в рамках стандартного процесса QA (quality assessment).

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

Системы управления переводами


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

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

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

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

Давайте внимательнее рассмотрим некоторые другие узкие места: предупреждён значит вооружён!

Фактор узкого места 2: Кодировка переводов

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

Мы рекомендуем всегда, по возможности, использовать Юникод вместо ASCII. А именно, наиболее распространённую и компактную кодировку UTF-8. Так что, пожалуйста, убедитесь, что передаёте локализационные файлы в правильной кодировке.

Фактор узкого места 3: жёстко закодированные строки

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

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

Фактор узкого места 4: числовые форматы и единицы измерения

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

Нет: Мама съела + %num + яблок.
Да: Мама съела %num + яблок.

Фактор узкого места 5: ошибки интерфейса

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

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

Ещё один совет: делайте дизайн интерфейса, опираясь на худший с точки зрения возможной длины текста язык локализации. Например, немецкая версия текста в среднем на 30% длиннее английской, а текст на русском языке в среднем на 10% длиннее. То же самое обычно справедливо для арабской локализации. Между тем традиционные китайские иероглифы, как правило, требуют на 30% меньше места, чем латинский алфавит.

5. Соберите команду по локализации и организуйте процесс


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

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

Мы надеемся, что эти советы были полезны, и желаем вам удачи в ваших проектах по локализации!

Об авторе

Статья написана в Alconost.

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

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

Подробнее
Подробнее..

Приглашаем на Mobile Meetup Innopolis

30.06.2020 18:10:31 | Автор: admin


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


Все ли вы знаете об Android Jetpack?



Кирилл Розов, Mobile Lead, Replika / Android Broadcast


Android Jetpack развивается невероятными темпами и уже есть в любом современном Android-приложении. Сейчас сложно представить разработку без этого набора библиотек. Уследить за всеми новинками непросто, поэтому я сделаю обзор последнего API и будущего библиотеки AndroidX.


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


Приглашаю послушать доклад практикующих Android-разработчиков. Вы узнаете, как сделать интеграцию Dagger 2 c Fragment (без Hilt) и какие API из KTX представляют опасность для использования.


Шаблоны проектирования Server Driven UI



Никита Русин, Platform lead, БюроБюро


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


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


Разберём:


  • Что такое Hypermedia API и Server Driven UI.
  • Каким образом при небольшой подготовке можно клепать 100500 информационных экранов в день, не меняя клиентский код.
  • Способы реализации SD UI на сервере и на клиенте вместе с простыми примерами на Python и Kotlin.
  • Как подготовиться к тому, чтобы на SD UI реализовывать целые пользовательские сценарии/новые разделы без изменений кода клиента.

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





Вместе со зрителями обсуждать доклады будут модераторы Юрий Новак, ведущий системный аналитик в компании РТ Лабс (Иннополис) и Александр Симоненко, технический директор Технократии (Казань).


Начинаем 2 июля в 17:00


Регистрация напомним о встрече за пару часов


Ссылка на трансляцию


Телеграм-чат митапа


До встречи!

Подробнее..

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

21.06.2020 14:07:34 | Автор: admin
В новом дайджесте снова разбираемся с темными темами, с монополизмом Apple, с недавним релизом Android 11 и сложностями тестирования, с границами UX и масштабированием загрузок.


С точки зрения методологии в образовательных проектах есть интересная деталь: мы используем в обучении два подхода индивидуальный и командный. Одни преподаватели выстраивают программу курса, исходя из плотной командной работы, другие, наоборот, опираются на индивидуальную работу каждого студента. Но, оставив в стороне рассуждения об эстетике тёмной темы, так ли уж она полезна для глаз? На самом ли деле тёмная тема повышает продуктивность работы с текстом? Ралука Будиу (Raluca Budiu) из Nielsen Norman Group даёт исчерпывающие ответы.

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

iOS

(+9)simctl: управление симуляторами Apple через терминал
(+3)Sign in with Apple дедлайн уже 30 июня
(0)HorizontalList с помощью SwiftUI
(0)SwiftUI по полочкам: Анимация, часть 2 тайминги
Apple открыла новый форум для разработчиков
Создатели почтового клиента Hey обвинили Apple в вымогательстве +Match Group и Epic Games поддержали разбирательство с Apple
Оборот приложений в App Store в 2019 году превысил $500 млрд.
Объяснение CAGradientLayer
6 советов по повышению производительности Core Data
Как в реальном времени обрабатывать изображения с камеры iOS
Как реализовать Динамический загрузчик с Lottie и Firebase
Создайте свою собственную библиотеку кнопок с нуля в SwiftUI
MemoryLayout в Swift
Создаем потрясающие индикаторы загрузки с помощью SwiftUI
HorizonCalendar: календарь от Airbnb
MultiProgressView: анимированные прогресс-бары

Android

(+15)Android 11 Beta и обновления для разработчиков
(+18)Litho: лучшие практики для создания эффективного UI в Android
(+6)Как отключить предупреждение о вреде долгого прослушивания аудио (Android)
(+5)Android Camera2 API от чайника, часть 6. Стрим видео сначала кодировали, теперь декодируем
(+5)MVP для Android преимущества использования Moxy в качестве вспомогательной библиотеки
(+4)Настраиваем GitHub Actions для Android с последующим деплоем в PlayMarket
(+1)Как и зачем мы используем несколько движков карт в inDriver
(+1)Android-разработка: Карьерный обзор за май 2020
По следам Android 11 Beta
Исследуем новую Google Play Console: большой шаг вперед
Вышла Google Play Billing Library Version 3
Start в Android с Дмитрием Виноградовым
Выбор правильного лейаута для Android
AndroidX: App Startup
Изучение внедрение зависимостей в Android Dagger, Koin и Kodein
Практическое руководство для решения OutOfMemoryError в Android-приложении
Зачем нам нужен Jetpack Compose?
WebRTC на Android: как включить аппаратное кодирование на нескольких устройствах
Что нового в Android Studio System Trace
Делаем снукер для Android с анимацией на основе физики
Представляем Pixel: новую Kotlin-библиотеку загрузки изображений для Android
Amaz Timer: таймер для умных часов
Meow Framework: MVVM и материальный дизайн

Разработка

(+28)Как мы обвесили механику баллистического расчета для мобильного шутера алгоритмом компенсации сетевой задержки
(+21)Многократное использование UI-компонентов в масштабах организации
(+15)Как сократить оформление ущерба по ОСАГО с нескольких дней до 60 минут
(+14)Оля, тесты и фабрика путь к красивой архитектуре и чистоте кода
(+5)Async/await в Unity
(+3)Хочешь, чтобы тебе поставили корректную дизайн-задачу? Помоги продакту ее поставить
(+2)Как устранить слепые зоны с помощью визуального тестирования
(+2)Обучение умных игровых соперников в Unity методом игра с самим собой средствами ML-Agents
(+1)Маски тестировщика (вопросы для успешного перехода к тестерскому расстройству личности)
Podlodka #168: геймификация процессов
Дизайн приложений: примеры для вдохновения #5
5 сервисов для управления мобильными подписками
Вы не Google
Чрезмерно сложно? Слишком просто? Эффективная граница UX
8 советов, как по-быстрому улучшить свой интерфейс
Как сделать ui анимацию естественной и приятной глазу: физические законы в анимации интерфейсов на практике
Swift или Kotlin что лучше?
Добавление облачной функции обнаружения объектов к системе домашних камер
Классические ошибки, которые совершил каждый разработчик
Как скрыть ваши API ключи
10 идей из руководства Apple по разработке пользовательского интерфейса
Редизайн банковского приложения с неоморфизмом

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

(0)Быстрый лайфхак для роста приложений ASO на других языках
Как я масштабировал приложение с 0 до 100,000 загрузок без единого потраченного доллара
Мобильные магазины показывают невероятный уровень вовлечения продажи в 2020 году выросли на 40%
Google запустил аналог Pinterest социальную сеть Keen
ФАС отказалась от смягчения предустановки российских приложений
Исследование: объем российского рынка мобильных игр вырос на 49% в 2019 году
ASO аудит приложений и универсальные рекомендации
myTarget добавили новые метрики в аналитике рекламных кампаний
The Pokemon Company учит детей чистить зубы с Pokemon Smile
Spike получил $8 млн. на то, чтобы сделать почту похожей на чат
Мультиформат, квадратное и вертикальное видео: лучшие форматы мобильной рекламы для получения установок
Антикейс: почему расходится количество конверсий в Google Ads и в аналитике для iOS-приложения
Что влияет на позиции приложения в App Store и Google Play

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

(+123)Самая сложная задача в Computer Vision
(+46)ABBYY NeoML: как мы делали библиотеку машинного обучения и зачем она нужна
(+37)Как понять, что нейросеть решит вашу проблему. Прагматичное руководство
(+37)Полный цикл создания устройства и работа сфабриками в Китае. Доклад Яндекса
(+19)Древности: десятилетие Apple iPad
(+14)Event2Mind для русского языка. Как мы обучили модель читать между строк и понимать намерения собеседника
(+8)Умный дом в умном городе
Huawei впервые стал 1 на рынке смартфонов
Можно ли удалить 99% нейронной сети без потери точности?

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

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

28.06.2020 14:08:19 | Автор: admin
В этом выпуске, конечно, у нас много материалов с WWDC App Clips, виджеты, новый Xcode, macOS Big Sur, изменения в StoreKit, UIKit и SwiftUI. Кроме них стилизация, полезные инновации, логистика, анимации и многое другое.


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

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

iOS

(+16)Тестирование StoreKit в Xcode 12 и iOS 14
(+15)SwiftUI 2020. Что изменилось?
(+10)Как и к чему готовиться на собеседование начинающему iOS-разработчику и не только
(+6)iOS in-app purchases: Конфигурация и добавление в проект
(+5)Выступления и презентации в стиле Apple: на примере WWDC20
iOS 14 позволит получать уведомления о важных звуках
Core ML теперь можно шифровать и обновлять независимо от приложения
Загрузка данных в iOS в Background-режиме
Что нового в Xcode 12
Тестирование покупок и семейная подписка: обновление IAP с WWDC
Apple пропустила Hey в App Store и меняет политику модерации
Создавайте виджеты с помощью WidgetKit
Apple представила мини-приложения App Clips
Apple запустила программу поддержки разработки универсальных приложений
Apple представляет macOS Big Sur
watchOS 7: новые настройки и функции для тех, кто следит за здоровьем
Apple раскрывает новые возможности iPhone с iOS 14
iPadOS 14: новые функции, созданные специально для iPad
Что iOS-разработчики думают о переходе Mac на процессоры Apple и других анонсах WWDC 2020
Рисуем в 3D, используя SwiftUI
iOS 14: важные изменения в UIKit
Автоматизация создания скриншотов в iOS с Bitrise и Fastlane
Цепная анимация в Swift
Создаем SwiftUI + Core ML игру для iOS
Как анимировать изображения в Swift
Как Apple делает soft-UI будущим

Android

(+9)Стилизация Android-приложений и дизайн-система: как это сделать и подружить одно с другим
(+3)Android и 3D камера. Распознавание лиц с защитой от Fraud
(0)Кодовая база. Расширяем RecyclerView
ARCore Depth API открыли для всех
Победители конкурса Полезные Инновации от Google
Android Broadcast: все тайны MVI
Huawei выпустил HMS Core 5.0
Создание безопасных Android-приложений
Мигрируем с Retrofit на Ktor
Внедрение темной темы в ваше Android-приложение
Как создать REST API для вашего приложения с помощью Spring Boot, Kotlin и Gradle
Создание масштабируемой навигационной системы в Android
Отточенный и гибкий Progress View для Android
Шаблон моего Android-проекта
Бифуркация Android
Merge Adapter: объединяйте списки в Android
Освоение шаблонов дизайна в Android с Kotlin
Создание адаптера RecyclerView, который можно использовать с любыми данными и любым представлением
Wizard Camera: эффекты для фото на OpenGL
Checked Android App: ToDo на Kotlin
CornerSheet: расширяемое окно

Разработка

(+54)Как мы сэкономили время курьерам. Логистика в Яндекс.Еде
(+26)Создание шейдерной анимации в Unity
(+21)Мобильные антивирусы не работают
(+15)Разбор UI/UX на примере прототипа в Figma и основные принципы
(+7)Создаем прогрессивное веб-приложение на ReactJS и размещаем его в Netlify и PWA Store
(+7)Как устроен Selenium: Эпизоды 3 5
(+6)Usability Testing от А до Я: подробный гид
(+3)Разработка мобильных приложений: как формируется цена?
(+3)UX/UI-ДИЗАЙН: нельзя просто взять и нарисовать экран
Podlodka #169: увольнения
В AWS запустили конструктор приложений Amazon Honeycode
Unity сделала все Premium курсы бесплатными
Дизайн приложений: примеры для вдохновения #6
ML Kit становится отдельным продуктом
Серьезные ошибки в UX, которые могут снижать ваши продажи
Мое 10-летнее путешествие в разработке игр
13 моих любимых UI/UX ресурсов
Быстрая навигация во Flutter с Get

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

(+1)Локализация мобильных приложений: основные сложности и лайфхаки
(0)Получение данных Amplitude через API
Одноклассники выплатили создателям мобильных игр более 360 млн. рублей
myTarget расширил возможности закупки видеорекламы в форматах Rewarded и Interstitial видео
9 способов повысить точность прогноза дохода
Kaia Health: физиотерапия под присмотром приложения
TikTok вкладывает $50 млн. в образовательный контент
Приложение для детей: от идеи до запуска
Пять лайфхаков по оптимизации приложения в App Store и Google Play

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

(+19)От AI до VR: как промышленность и ритейл используют новые технологии
(+15)Управление Яндекс.Станцией и другими колонками с Алисой из Home Assistant
(+10)Опыт построения умного дома на Raspberry Pi и открытой платформе OpenHAB. Часть 1
(+9)HMI на основе Node-red и Scadavis.io
(+3)Из чего состоит набор для разработчиков NB-IoT DevKit?

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

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

06.07.2020 06:04:37 | Автор: admin
В новом дайджесте разбираемся с последствиями WWDC, выпускаем приложения одной кнопкой, сравниваем быстродействие кроссплатформенных фреймворков, проводим нагрузочное тестирование, растим доходы приложений и занимаемся многими другими интересными вещами!


Сегодня я подведу некоторые итоги: расскажу, к чему мы пришли за это время. Long story short: любой причастный к процессу сотрудник может зарелизить хоть все наши приложения на обеих платформах в несколько кликов без головной боли, больших затрат времени, регистрации и СМС. Так, наш отдел релиз-инженеров за 2019 год сэкономил около 830 часов. inVerita и ее команда разработчиков мобильных приложений постоянно изучают производительность кроссплатформенных мобильных фреймворков, доступных на рынке, чтобы ответить на вопрос, какая технология лучше всего подходит для вашего продукта.

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

iOS

(+27)Бюджетный DI на антипаттернах
(+21)Почему разработчики отказываются от авторизации через Apple с фейковым email
(+6)BoxView удобный autolayout для iOS
(+4)Как смотреть WWDC 2020, если ты не разработчик
(+3)Apple WWDC 2020: что нового в тестировании iOS
(+3)Развитие ARKit в этом году и новая возможность в ARKit 4: Location Anchors
Презентации WWDC и Platforms State of the Union доступны с субтитрами на русском языке
Apple пытается улучшить вовлеченность игр Arcade
Apple назвала 8 лауреатов ежегодной премии Apple Design Awards
Погружаемся в мир дополненной реальности с ARKit
Когда вам нужно сообщить о применении шифрования в приложении?
Новое в iOS 14: определение контуров
iOS 14 App Clips
Ваше первое сложное приложение на SwiftUI
Внедряем чистую VIP-архитектуру в Swift 5
Создайте свою собственную библиотеку CocoaPods
Как создавать виджеты в iOS 14
Новый жизненный цикл и замены для AppDelegate и SceneDelegate в SwiftUI в iOS 14
NewYorkAlert: красивые предупреждения для iOS

Android

(+12)Редактор кода на Android: часть 1
(+5)Приручая MVI
(+5)Блокировка двойного клика. Велосипед?
Huawei объявляет конкурс приложений с призовым фондом в 1 млн. долларов
Представляем RainbowCake
JetPack Compose с Server Driven UI
Новый способ передачи данных между Фрагментами
Динамическое изменение цвета градиента в Android
Создаем приложение с новостями для Android за 5 простых шагах
MVVM с Hilt, RxJava 3, Retrofit, Room, Live Data и View Binding
Быстрое тестирование на Android с Mobile Test Orchestrator
Кастомные Android View: Drag and Drop
Дилемма Kotlin: Extension или Member
Рендеринг PDF-файлов на Android: простой способ
Укрепление безопасности системы в Android 11
Юнит-тестирование кастомных View в Android
Оптимизация работы с батареей для избежания Doze Mode и App Standby
Видимость пакетов в Android 11
RainbowCake: новая Android-архитектура
Разработка с Actions Builder и Actions SDK

Разработка

(+21)NewNode децентрализованная CDN от разработчика FireChat
(+9)Как мы решаем проблему отсутствия UI\UX дизайна в 1С с помощью Java Script и React.js
(+6)Godot, 1000 мелочей
(+5)Оптимизация SQL запросов или розыск опасных преступников
(+4)6 советов по нагрузочному тестированию к Черной пятнице
Podlodka #170: искусство простых иллюстраций
Flutter Dev Podcast #17: Flutter Day 2020
Думай, как CEO: самый важный навык, который выделит тебя среди разработчиков
Dfinity открывает платформу Internet Computer для разработчиков
Дизайн приложений: примеры для вдохновения #7
AWS запускает CodeGuru для автоматического анализа кода
7 подходов к тестированию
Мы упростили сайт до приложения с действием в один клик и провалились
8 правил, которые помогут вам спроектировать лучший дизайн карточки
Возврат скевоморфизма
14 популярных программ для создания анимации, прототипирования и дизайна интерфейсов
Полгода ежемесячного создания игр
Простой игровой движок с Flutter Animations
47 ключевых уроков для UI и UX дизайнеров
Возглавляя команду разработчиков программного обеспечения
20 лучших идей для дизайна пользовательского интерфейса
Мой опыт создания приложения с no-code инструментами
Давайте сделаем мобильную многопользовательскую игру на Unity
Делаем музыкальный плеер, играющий в фоновом режиме, на Flutter
7 инструментов для удаленных команд 2020
GetStorage: быстрое key-value хранилище
Fluent System Icons: мобильные иконки от Microsoft

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

(+6)Как работать с Google Analytics и Яндекс Метрикой?
(+6)Сколько стоит сделать ролик об игре своими силами
(+3)Материалы с митапа для аналитиков: модель роста, A/B-тесты, управление стоком и доставкой товаров
Яндекс дарит подключившимся к РСЯ до 600,000 рублей на продвижение приложений
Рейтинг доступности банковских приложений 2020 от UsabilityLab
Расходы на мобильные приложения в 1 половине 2020 выросли на 23.4%
TikTok заподозрили в шпионаже
В Индии забанили TikTok и десятки других китайских приложений
Сигнал от звёзд: Газпром-медиа запустил приложение с персональным гороскопом
Как работать с восточными языками в App Store и Google Play

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

(+8)9 ключевых алгоритмов машинного обучения простым языком
(+1)Опыт создания облачного решения по мониторингу цифрового киоска на Azure IoT Central
Niantic делает AR-проект по настольной игре Колонизаторы
Машинное обучение поможет спасать людей на пляжах

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

Litho лучшие практики для создания эффективного UI на Android

18.06.2020 20:20:38 | Автор: admin
Litho UI-фреймворк от Facebook, который отвечает за быстрый рендеринг тяжелого UI в топовых приложения с миллиардами загрузок.

Как его использовать, что происходит под капотом, и действительно ли с UI можно работать только из одного потока?


Cookbook по Litho в расшифровке моего доклада с конференции Mobius 2019 Moscow под катом.

С вами Сергей Рябов разработчик из Лондона, который поставил на паузу свой кочевой образ жизни, чтобы проверить, такой ли Альбион туманный, как о нём любят говорить. И я работаю в команде Native UI Frameworks над декларативным фреймворком Litho.

Структура поста:




Что такое Litho?




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

Когда мы работаем с интерфейсом, у нас есть следующие этапы работы: Inflate, Measure, Layout, Draw. Все эти этапы построения UI должны поместиться в 16 миллисекунд, чтобы приложение не тормозило. Предположим, что у нас тяжелый UI, который не укладывается в 16 миллисекунд: все оптимизации проведены, мы попытались выжать максимум производительности из наших вьюшек, но программа все равно лагает. Можно постараться что-нибудь вынести в фоновый поток, например, Inflate, поскольку это достаточно тяжелая операция. Для этого у нас есть AsyncLayoutInflater, из набора библиотек Android Jetpack.

Заметим, что AsyncLayoutInflater имеет некоторые нюансы: мы не можем создавать вьюшки, которые работают внутри с Handler, нам нужно, чтобы LayoutParams были thread-safe и некоторые другие. Но в целом использовать его можно.

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

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



Litho дает возможность абстрагироваться от View в фазах Measure и Layout благодаря тому, что под капотом для измерений используется движок Yoga, который тоже разрабатывается в команде Native UI Frameworks. И как следствие, это позволяет всю математику вынести в бэкграунд-поток.

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

Декларативный API в Litho


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



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

fun f(    title: String,    subtitle: String): UI {  return Column.create()    .child(        Text.create()            .text(title))    .child(        Text.create()            .text(subtitle))    .build()}

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

@LayoutSpecobject ListItemSpec {  @OnCreateLayout  fun onCreateLayout(      c: ComponentContext,      @Prop title: String,      @Prop subtitle: String  ): Component {    return Column.create(c)        .child(            Text.create(c)                .text(title))        .child(            Text.create(c)                .text(subtitle))        .build()   }}

Разница в том, что над функцией появляется аннотация @OnCreateLayout, которая сообщает, за что отвечает эта функция. Входящие свойства тоже помечаются специальной аннотацией @Prop, чтобы по ним сгенерировать правильный Builder, для конструирования UI, а также везде прокидывается специальный контекст ComponentContext. И всё это помещается в класс с аннотацией @LayoutSpec, который может содержать и некоторые другие методы.

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


Комбинирование UI-элементов


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



Решение похоже на использование горизонтального LinearLayout, но в данном случае мы горизонтально располагаем картинку и предыдущий UI-компонент ListItem, заворачивая их в Row. Также добавился еще один входной параметр @Prop image, который отвечает за саму картинку, а те параметры, которые отвечают за текстовые данные, просто прокидываются в компонент ListItem.

Стоит отметить, что описываются UI-компоненты в классах с суффиксом Spec, а для создания инстансов этих компонентов используются классы без этого суффикса. Всё потому, что Litho по декларативным Spec-файлам генерирует настоящие реализации компонентов с правильной обработкой многопоточности и удобным API в виде Builder-ов, которые и используются для задания входных параметров, объявленных в UI-спеке.

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



В публичном API Litho есть только одна View LithoView. Это контейнер, в котором идет отрисовка всех Litho-компонентов. Чтобы отобразить на экране заданный компонент, нужно создать ComponentContext, передав ему Android Context, и создать LithoView, передав в метод create контекст и сам отображаемый компонент. С LithoView можно делать всё, что вы привыкли делать с другими вьюшками, например, передать в метод setContentView у Activity.

С API на основе Builder-ов работать легко, механика создания UI-компонента похожа на описание вью в XML. Разница лишь в том, что вместо проставления XML-атрибутов вы вызываете методы Builder-а. Но раз уж это все так сильно отвязано от Android-системы, то что же происходит под капотом?


Litho: под капотом


Возьмем ListItemWithImageSpec, с которым мы уже встречались ранее. В нём три компонента: Row, Image и кастомный ListItem:

@LayoutSpecobject ListItemWithImageSpec {     // ...    Row.create(c)        .child(            Image.create(c)                  .drawable(image))        .child(            ListItem.create(c)                  .title(title)                  .subtitle(subtitle))        .build()  }}

И чтобы отобразить его на экране, добавим его в LithoView таким образом:

setContentView(LithoView.create(c,    ListItemWithImage.create(c)        .image(user.avatar)        .title(user.name)        .subtitle(comment.formatDate())        .build()))

Рендеринг UI-компонента проходит в три основных шага:

  1. Построение Internal Tree внутреннего представления UI.
  2. Получение LayoutState набора Drawable и View, которые будут отрисованы на экране
  3. Отрисовка LayoutState на Canvas-е

Построение Internal Tree


Начинаем с корневого компонента ListItemWithImage:

  • Создаем корневой элемент дерева, InternalNode, ассоциируем с ним компонент ListItemWithImage. Так как ListItemWithImage это фактически просто обертка, то смотрим на его содержимое.
  • Внутри метода onCreateLayout в ListItemWithImageSpec мы первым делом создаем Row. Ассоциируем его с той же самой нодой.
  • У Row 2 потомка: Image и ListItem для обоих создаются отдельные InternalNode-ы. Image это листовой элемент дерева компонентов, на этом обработка его поддерева окончена.
  • ListItem в свою очередь тоже компонент-обертка, чтобы добраться до сути смотрим в метод onCreateLayout его спеки. Там мы видим Column, ассоциируем её с той же нодой.
  • У Column есть 2 потомка: Text и Text создаём для них две новые ноды. Оба элемента листовые построение Internal Tree окончено.

Получилось следующее дерево:



Тут наглядно видно, что в результате ноды создаются только для листовых компонентов, таких как Image и Text, или для компонентов-контейнеров, таких как Row и Column. Так мы упростили иерархию: было три уровня относительно корня, осталось два.

Получение LayoutState


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

Происходит это следующим образом: мы снова обходим Internal Tree, и смотрим, нужно ли отрисовывать каждую следующую ноду. Row не отрисовывается, он фактически нужен был только для измерения и размещения дочерних элементов. Image отрисовывается, поэтому добавляем его LayoutOutput в список. Column тоже нужен был только для того, чтобы померять и расположить элементы и отрисовывать там нечего, а вот Text-ы, как и Image, тоже важны для отрисовки.

Получившийся в итоге список LayoutOutput-ов это наш LayoutState.

Отрисовка LayoutState


Теперь полученный LayoutState осталось нарисовать на экране. И тут важно подчеркнуть, что в данном примере элементы Image и два Text-a будут отрисованы не с помощью View, а с помощью Drawable. Если мы можем не использовать сложные элементы Android UI Toolkit, если можно обойтись простыми и легковесными примитивами типа Drawable, то эффективнее использовать именно их. Если же какие-то элементы должны уметь реагировать, например, на клики, то они будут отрисованы с помощью View, чтобы переиспользовать всю непростую логику обработки UI-событий.



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

На рассмотренном примере мы познакомились с несколькими ключевыми элементами:

  • @LayoutSpec компоненты, комбинирующие другие компоненты. В итоге они превращаются в поддеревья в Internal Tree. Аналог кастомных ViewGroup.
  • Row и Column компоненты-контейнеры, служащие для задания расположения UI-элементов на экране. Это примитивы Flexbox основного Layout Engine в Litho. А Yoga это его кроссплатформенная реализация, которая используется не только в Litho, но также и в других библиотеках под Android, под iOS и в Web.
  • @MountSpec это те самые листовые ноды в Internal Tree Image, Text и другие. Это второй тип Spec, который описывает примитивные элементы, которые будут отрисованы на экране с помощью Drawable или View.

Как будет выглядеть код кастомной @MountSpec-и? Примерно так:

@MountSpecobject GradientSpec {  @OnCreateMountContent  fun onCreateMountContent(context: Context): GradientDrawable {      return GradientDrawable()  }  @OnMount  fun onMount(      c: ComponentContext,      drawable: GradientDrawable,      @Prop @ColorInt colors: IntArray) {      drawable.colors = colors  } }

В данном примере мы берем некоторый массив цветов и отрисовываем созданный на его основе градиент на экране. В андройде для работы с градиентами есть специальный GradientDrawable. Именно его мы и используем для рендеринга этого компонента. Инстанс данного типа нужно вернуть из специального lifecycle-метода, который помечается аннотацией @OnCreateMountContent и отвечает за создание контента для рендеринга.

Напомню, что компонент, описанный как MountSpec, может отрисовывать всего два типа контента: View или Drawable. В данном простом случае нам достаточно легковесного Drawable. Кроме операции создания контента мы также должны определить метод с аннотацией @OnMount для биндинга с данными перед тем, как компонент станет видимым. В нашем случае данными является тот массив цветов, который мы получаем на вход. Всё остальное Litho берет на себя и отрисовывает GradientDrawable c заданными цветами на экран. Для облегчения понимания можно сравнить методы, помеченные @OnCreateMountContent и @OnMount, с методами RecyclerView.Adapter onCreateViewHolder и onBindViewHolder соответственно.


Аннотация @MountSpec


В аннотации @MountSpec есть два параметра:

  1. poolSize параметр, который отвечает за то, сколько инстансов данного компонента будет создано заранее и помещено в пул, чтобы потом быстро использовать их при рендеринге интерфейса. По умолчанию этот параметр равен 3.
  2. isPureRender это булевый флаг, показывающий, что при пересоздании компонента с неизменными значениями Prop-параметров, результат его отрисовки всегда будет оставаться прежним. При обновлении UI-дерева компонентов это позволяет не пересоздавать и не перерисовывать такие чистые компоненты.

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

@MountSpec (poolSize = 30, isPureRender = true)class ImageSpec {  @ShouldUpdate  static boolean shouldUpdate(...) {}}public @interface MountSpec {  int poolSize() default 3;  boolean isPureRender() default false;}

У него очень большой poolSize (30). Cегодня типичная ситуация, когда приложение нагружено картинками, поэтому UI-компоненты для них лучше подготовить заранее в достаточном количестве. В то же время, если входной параметр Drawable, не меняется, то и вывод на экран такого компоненты тоже не поменяется, и, чтобы не делать лишних действий, можно установить флаг isPureRender В этом случае решение об обновлении компонента принимается на основании сравнения Prop-параметров с помощью equals(), если же вы хотите использовать кастомную логику сравнения, то её нужно поместить в функцию с аннотацией @ShouldUpdate.


Оптимизации в Litho


В Litho есть две ключевые оптимизации при построении Layout:

  1. Layout/Mount Diffing позволяет переиспользовать размеры (measurements) элементов с предыдущего рендеринга и переиспользовать LayoutOutput-ы (то, что выводится на экран) с предыдущего рендеринга.
  2. IncrementalMount позволяет превратить ваш UI в RecyclerView на стероидах без каких-либо дополнительных усилий.

Layout/Mount Diffing


Как это работает? При построении Internal Tree для нового компонента, также учитывается оставшееся с предыдущего рендеринга Diff Tree с готовыми размерами и LayoutOutput-ами всех узлов дерева.



Если входящие параметры для некоторого поддерева не изменились, то размеры и LayoutOutput для него просто копируются из Diff Tree в новый Internal Tree, минуя шаг измерения с помощью Yoga. Таким образом, LayoutState готов уже к концу построения Internal Tree.

IncrementalMount


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

mount
Mount процесс получения контента для рендеринга (View или Drawable) и его добавление в текущую иерархию View

такого сложного элемента не уложится в 16мс, то мы будем видеть дерганый UI, особенно при быстрой прокрутке. IncrementalMount в этом случае позволяет рендерить новостной пост не целиком, а постепенно, выполняя mount только для тех дочерних примитивных элементов, которые действительно попадают в видимую область экрана. А для тех же элементов, которые покидают её, выполнять unmount и возвращать их в пул, не дожидаясь, пока весь пост скроется за краем экрана, таким образом экономя память. Скролл существенно ускоряется за счёт того, что отрисовка тяжёлого поста разбивается на несколько фреймов. Всё это напоминает работу RecyclerView, но в Litho вам не надо как-то по-особому менять UI или использовать другие компоненты это работает из коробки.

Выводы на заметку:


Если вы определяете кастомную MountSpec-у, то:

  • можно использовать параметр isPureRender и метод @ShouldUpdate, чтобы не делать лишнюю работу при обновлении UI компонента.
  • зная, в каком объёме компонент будет использован в приложении, вы можете подготовить нужное количество инстансов заранее, настроив размер пула с помощью poolSize.


Управление Состоянием


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

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



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

Затем нам нужен метод для изменения стейта. Он помечается аннотацией @OnUpdateState и на вход принимает все тот же самый стейт, но не в виде неизменяемого значения, а завернутый в холдер StateValue, в котором стейт реально можно поменять.

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



В данном примере видно, что наборы параметров в описании метода и в месте его вызова не совпадают. Это происходит, потому что вы вызываете метод, определенный в Spec-классе не руками, а неявно, через сгенерированный метод в сгенерированном Component-классе, и все значения, необходимые для Spec-метода (в данном случае, StateValue), Litho подставит сам.

Каждое обновление стейта вызывает тот же эффект, что и передача новых значений Prop-параметров: снова нужно построить Internal Tree, получить LayoutState и отрисовать его на экран.

А что если у нас есть пара переменных для состояния и разные для них методы обновления? Допустим, у нас есть профиль супергероя, в котором нам надо поменять цвет и имя. Мы хотим сменить зеленого Халка на красного Железного Человека. У нас есть две переменные состояния color и name, и мы делаем два обновления путем присвоения переменных.

@OnUpdateStatefun changeColor(color: StateValue<Color>, @Param newColor: Color) {  color.set(newColor)}@OnUpdateStatefun changeName(name: StateValue<String>, @Param newName: String) {  name.set(newName)}...RootComponent.changeColor(c, Color.RED)RootComponent.changeName(c, "IronMan")

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

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



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

@OnUpdateStatefun changeHero(    color: StateValue<Color>, name: StateValue<String>,    @Param newColor: Color, @Param newName: String) {  color.set(newColor)  name.set(newName)}...RootComponent.changeHero(c, Color.RED, "IronMan")

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

Отложенное обновление состояния


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



Для шага заведем отдельный State-параметр step, в котором будем хранить текущее значение, и сделаем возможность вводить его с клавиатуры в поле TextInput. Так как при изменении этого значения в поле ввода новое число мы увидим сразу, то обновлять UI с новым значением step не надо, но запомнить его необходимо. Для этого надо выставить флаг canUpdateLazily, давая Litho понять, что этот State можно изменять без перестроения UI, лениво. В этом случае, помимо всех явно определенных @UpdateState методов, которые отвечают за обычные обновления состояния, сгенерируется ещё один метод lazyUpdateStep, выполняющий как раз такое ленивое обновление step. Префикс lazyUpdate общий для всех таких методов, а суффикс (Step) однозначно соответствует имени State-переменной.

@State(canUpdateLazily = true) step: IntRootComponent.lazyUpdateStep(c, value)

Выводы на заметку



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


Анимация в Litho


Давайте теперь перейдем от статического отображения UI к динамическому как в декларативном API Litho будет выглядеть описание анимации?



Рассмотрим простой пример (видео доклада 28:33-28:44) с кнопкой, которая меняет своё расположение при клике. Она прижимается то к правому, то к левому краю, но происходит это моментально, скачкообразно в этом случае пользователю не очевидно, что произошло.

Однако мы можем это исправить, добавить контекста и анимировать кнопку. Для этого надо сделать две вещи: надо пометить то, ЧТО мы анимируем, и описать, КАК надо анимировать. Мы двигаем кнопку, поэтому задаем ей свойство transitionKey.

Button.create(c)    .text(Catch me!)    .transitionKey(button)    .alignSelf(align)    .clickHandler(RootComponent.onClick(c))

Затем реализуем метод с аннотацией @OnCreateTransition, который создаёт описание анимации изменений между двумя отрисовками этого компонента, Transition между предыдущим и следующим состоянием UI. В данном случае Transition простой: мы создаём его с тем же transitionKey, которым пометили цель анимации (в данном случае, кнопку), и просим проанимировать только изменения горизонтальной координаты цели координаты X кнопки. В результате оно действительно анимируется (видео доклада 29:25-29.33).

@OnCreateTransitionfun onCreateTransition(c: ComponentContext): Transition {  return Transition.create("button")      .animate(AnimatedProperties.X)}

Такое описание анимации отлично, если вы четко знаете, что нужно анимировать и хотите полного контроля над тем, как это анимировать, но может стать многословным в сложных компонентах. Если же вы хотите проанимировать любые изменения в layout-е и сделать это автоматически, то в Litho есть специальный Transition.allLayout() для этих целей. Это нечто похожее на установку animateLayoutChanges = true для анимации всех изменений в нативной ViewGroup.

@OnCreateTransitionfun onCreateTransition(c: ComponentContext): Transition {  return Transition.allLayout()}

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

Чтобы самим определять, надо или нет анимировать конкретное изменение компонента, можно использовать Diff для Prop и State-параметров.

Diff это такой холдер, который содержит одновременно и текущее, и предыдущее значение для конкретного Prop/State, давая возможность их сравнить произвольным методом и сделать вывод, стоит анимировать или нет.



И если вернуть null из @OnCreateTransition, то анимироваться ничего не будет. Более того, будет пропущен весть этап подготовки анимации, что положительно скажется на производительности.

@OnCreateTransitionfun onCreateTransition(c: ComponentContext,    @Prop prop: Diff<String>,    @State state: Diff<Boolean>): Transition? {  return if (canAnimate(prop, state)) Transition.allLayout() else null}

Обратите внимание, что и аннотации, и имена соответствующих Prop/State остаются такими же, как в @OnCreateLayout, меняется лишь тип с T на Diff.

Выводы на заметку


Используйте Diff параметры для более тонкой настройки анимации изменения значений Prop и State.


Пошаговое внедрение


Вряд ли в существующем проекте кто-то решится в одночасье переписать весь UI на Litho. Поэтому возникаю логичные вопросы: можно ли осуществлять внедрение по частям? Могут ли Litho-компоненты жить бок о бок с нативным Android UI? И тут у меня для вас хорошие новости!

Да, ваш сложный UI можно портировать на Litho по частям:

  • С одной стороны можно использовать Litho-компоненты внутри существующего UI на View. Можно последовательно заменять сложные UI-поддеревья в вашей разметке на LithoView с аналогичной иерархией компонентов. Таким образом вы упростите изначальный UI и уменьшите глубину дерева элементов.
  • С другой стороны можно использовать кастомные View сложные графики, анимированные круговые диаграммы, видео-плееры, которые нелегко переписать на компоненты, в Litho-интерфейсах. Для этого View нужно обернуть в MountSpec-у (помните, что метод с @OnCreateMountContent может возвращать не только Drawable, но и View?), которую потом легко можно будет включать в иерархию компонентов.


Дебаггинг и тулы в Litho


А что же нам делать, если вдруг что-то не будет работать? Если будут вопросы, то где смотреть примеры? Как отладить интерфейс на Litho? Как быстро верстать и тюнить UI? Обо всём этом ниже.

Yoga playground


Litho использует концепцию и терминологию Flexbox для задания расположения элементов. Если вы с ней не знакомы, то для вас есть Yoga Playground. Это интерактивная песочница, где на схематичном представлении UI с виде прямоугольников можно поиграться с параметрами, подготовить макет вашего UI и даже экспортировать его в виде Litho-кода на Java.

Flipper + Litho plugins


Для Litho, к сожалению, нет поддержки в UI Preview. Стандартный Layout Inspector тоже не сможет показать иерархию компонентов Litho. Всё потому, что эти инструменты работают только с Android View. Но к счастью коллеги из команды UI Tools разрабатывают замечательный инструмент для разносторонней отладки мобильных приложений Flipper. Layout-плагин для Flipper умеет отображать иерархию UI-элементов интерфейса, который отображается на экране телефона, и распознаёт не только обычные View, но и компоненты Litho. Кроме того, при выделении какого-либо компонента, в боковой панели можно увидеть список свойств Props компонента, большую часть из которых можно менять в реальном времени и проверять изменения на устройстве. Это сильно упрощает финальную подстройку UI, во многом заменяя UI Preview.



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

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



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

Litho IntelliJ plugin


В Litho сильно отличающийся от стандартного подход к написанию UI, свои аннотации и lifecycle-методы много нового. Есть, конечно, документация, но чтобы при написании каждой новой Spec-и не обращаться к ней для уточнения любых вопросов, а стартовать быстро, наша команда также предоставляет IntelliJ / Android Studio плагин. Он добавляет шаблоны для создания LayoutSpec и MountSpec, шаблоны для генерации отдельных lifecycle-методов, а также возможность навигации между Spec-классом и сгенерированным по нему классом компонента.



Плагин можно установить через IntelliJ Plugin Marketplace.

Lithography Sample app


Ну а кроме всего вышеназванного, конечно же в репозитории есть sample-приложение Lithography. В нём можно посмотреть рецепты по реализации каких-то реальных примеров: создать UI карточки, загрузить картинку из интернета, реализовать Fast Scroll. Есть целые секции по работе со списками, различным способам анимации и так далее.

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


Резюме


Ключевые достоинства Litho в том, что обработку UI можно частично проводить на фоновом-потоке, его декларативный API позволяет проще описывать все возможные состояния UI, а при рендеринге получаются более плоские иерархии View. Несмотря на то, что мы все оборачиваем в Row, Column и прочие компоненты, на самом деле рисоваться будут только листовые элементы дерева и каждый пиксель как правило будет рисоваться по одному разу. Incremental Mount предоставляет возможность более гранулярного переиспользования отдельных атомарных MountSpec, а не только целых LayoutSpec компонентов элементов списка.


Бонус: Litho и Kotlin


С учётом завязки Litho на процессинг аннотаций и кодогенерацию, использование его с Kotlin может дать некоторое замедление сборки, так как KAPT печально известен своей неторопливостью. Ну и чего скрывать, для такого модного молодежного языка, как Kotlin, обилие аннотаций в API не выглядит очень удобно, когда везде правят разнообразные DSL-и. А хотелось бы вот как-то так просто создать UI в одной функции, да может даже прямо в Activity, и там же его в Activity и отрендерить, без плясок с LithoView:

class PlaygroundActivity : Activity() {  override fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    setContent {      val counter by useState { 1 }      Clickable(onClick = { updateState { counter.value = counter.value + 1 } }) {        Padding(all = 16.dp) {          Column {            +Text(text = "Hello, Kotlin World!", textSize = 20.sp)            +Text(                text = "with ${"".repeat(counter.value)} from London",                textStyle = Typeface.ITALIC)          }           }      }    }  }}

Так вот всё это реальный код! Пока что Kotlin API находится в активной разработке, но экспериментировать с ним можно уже сейчас Kotlin-артефакты выкладываются с каждым релизом Litho, а кроме того доступны их Snapshot-версии. Также, вы можете следить за развитием проекта на Github-е.

Настоятельно рекомендую ознакомиться с материалами по ссылкам:


Уже на следующей неделе состоится Mobius 2020 Piter. Там для Android-разработчиков тоже будет много интересного: например, выступит хорошо знакомый им Chet Haase из Google. Многие помнят его по выступлениям на Google I/O, а в этом году I/O отменили, но благодаря Mobius есть шанс всё равно увидеть Чета и даже лично задать ему вопрос.
Подробнее..

Litho лучшие практики для создания эффективного UI в Android

18.06.2020 22:05:19 | Автор: admin
Litho UI-фреймворк от Facebook, который отвечает за быстрый рендеринг тяжелого UI в топовых приложения с миллиардами загрузок.

Как его использовать, что происходит под капотом, и действительно ли с UI можно работать только из одного потока?


Cookbook по Litho в расшифровке моего доклада с конференции Mobius 2019 Moscow под катом.

С вами Сергей Рябов разработчик из Лондона, который поставил на паузу свой кочевой образ жизни, чтобы проверить, такой ли Альбион туманный, как о нём любят говорить. И я работаю в команде Native UI Frameworks над декларативным фреймворком Litho.

Структура поста:




Что такое Litho?




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

Когда мы работаем с интерфейсом, у нас есть следующие этапы работы: Inflate, Measure, Layout, Draw. Все эти этапы построения UI должны поместиться в 16 миллисекунд, чтобы приложение не тормозило. Предположим, что у нас тяжелый UI, который не укладывается в 16 миллисекунд: все оптимизации проведены, мы попытались выжать максимум производительности из наших вьюшек, но программа все равно лагает. Можно постараться что-нибудь вынести в фоновый поток, например, Inflate, поскольку это достаточно тяжелая операция. Для этого у нас есть AsyncLayoutInflater, из набора библиотек Android Jetpack.

Заметим, что AsyncLayoutInflater имеет некоторые нюансы: мы не можем создавать вьюшки, которые работают внутри с Handler, нам нужно, чтобы LayoutParams были thread-safe и некоторые другие. Но в целом использовать его можно.

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

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



Litho дает возможность абстрагироваться от View в фазах Measure и Layout благодаря тому, что под капотом для измерений используется движок Yoga, который тоже разрабатывается в команде Native UI Frameworks. И как следствие, это позволяет всю математику вынести в бэкграунд-поток.

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

Декларативный API в Litho


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



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

fun f(    title: String,    subtitle: String): UI {  return Column.create()    .child(        Text.create()            .text(title))    .child(        Text.create()            .text(subtitle))    .build()}

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

@LayoutSpecobject ListItemSpec {  @OnCreateLayout  fun onCreateLayout(      c: ComponentContext,      @Prop title: String,      @Prop subtitle: String  ): Component {    return Column.create(c)        .child(            Text.create(c)                .text(title))        .child(            Text.create(c)                .text(subtitle))        .build()   }}

Разница в том, что над функцией появляется аннотация @OnCreateLayout, которая сообщает, за что отвечает эта функция. Входящие свойства тоже помечаются специальной аннотацией @Prop, чтобы по ним сгенерировать правильный Builder, для конструирования UI, а также везде прокидывается специальный контекст ComponentContext. И всё это помещается в класс с аннотацией @LayoutSpec, который может содержать и некоторые другие методы.

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


Комбинирование UI-элементов


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



Решение похоже на использование горизонтального LinearLayout, но в данном случае мы горизонтально располагаем картинку и предыдущий UI-компонент ListItem, заворачивая их в Row. Также добавился еще один входной параметр @Prop image, который отвечает за саму картинку, а те параметры, которые отвечают за текстовые данные, просто прокидываются в компонент ListItem.

Стоит отметить, что описываются UI-компоненты в классах с суффиксом Spec, а для создания инстансов этих компонентов используются классы без этого суффикса. Всё потому, что Litho по декларативным Spec-файлам генерирует настоящие реализации компонентов с правильной обработкой многопоточности и удобным API в виде Builder-ов, которые и используются для задания входных параметров, объявленных в UI-спеке.

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



В публичном API Litho есть только одна View LithoView. Это контейнер, в котором идет отрисовка всех Litho-компонентов. Чтобы отобразить на экране заданный компонент, нужно создать ComponentContext, передав ему Android Context, и создать LithoView, передав в метод create контекст и сам отображаемый компонент. С LithoView можно делать всё, что вы привыкли делать с другими вьюшками, например, передать в метод setContentView у Activity.

С API на основе Builder-ов работать легко, механика создания UI-компонента похожа на описание вью в XML. Разница лишь в том, что вместо проставления XML-атрибутов вы вызываете методы Builder-а. Но раз уж это все так сильно отвязано от Android-системы, то что же происходит под капотом?


Litho: под капотом


Возьмем ListItemWithImageSpec, с которым мы уже встречались ранее. В нём три компонента: Row, Image и кастомный ListItem:

@LayoutSpecobject ListItemWithImageSpec {     // ...    Row.create(c)        .child(            Image.create(c)                  .drawable(image))        .child(            ListItem.create(c)                  .title(title)                  .subtitle(subtitle))        .build()  }}

И чтобы отобразить его на экране, добавим его в LithoView таким образом:

setContentView(LithoView.create(c,    ListItemWithImage.create(c)        .image(user.avatar)        .title(user.name)        .subtitle(comment.formatDate())        .build()))

Рендеринг UI-компонента проходит в три основных шага:

  1. Построение Internal Tree внутреннего представления UI.
  2. Получение LayoutState набора Drawable и View, которые будут отрисованы на экране
  3. Отрисовка LayoutState на Canvas-е

Построение Internal Tree


Начинаем с корневого компонента ListItemWithImage:

  • Создаем корневой элемент дерева, InternalNode, ассоциируем с ним компонент ListItemWithImage. Так как ListItemWithImage это фактически просто обертка, то смотрим на его содержимое.
  • Внутри метода onCreateLayout в ListItemWithImageSpec мы первым делом создаем Row. Ассоциируем его с той же самой нодой.
  • У Row 2 потомка: Image и ListItem для обоих создаются отдельные InternalNode-ы. Image это листовой элемент дерева компонентов, на этом обработка его поддерева окончена.
  • ListItem в свою очередь тоже компонент-обертка, чтобы добраться до сути смотрим в метод onCreateLayout его спеки. Там мы видим Column, ассоциируем её с той же нодой.
  • У Column есть 2 потомка: Text и Text создаём для них две новые ноды. Оба элемента листовые построение Internal Tree окончено.

Получилось следующее дерево:



Тут наглядно видно, что в результате ноды создаются только для листовых компонентов, таких как Image и Text, или для компонентов-контейнеров, таких как Row и Column. Так мы упростили иерархию: было три уровня относительно корня, осталось два.

Получение LayoutState


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

Происходит это следующим образом: мы снова обходим Internal Tree, и смотрим, нужно ли отрисовывать каждую следующую ноду. Row не отрисовывается, он фактически нужен был только для измерения и размещения дочерних элементов. Image отрисовывается, поэтому добавляем его LayoutOutput в список. Column тоже нужен был только для того, чтобы померять и расположить элементы и отрисовывать там нечего, а вот Text-ы, как и Image, тоже важны для отрисовки.

Получившийся в итоге список LayoutOutput-ов это наш LayoutState.

Отрисовка LayoutState


Теперь полученный LayoutState осталось нарисовать на экране. И тут важно подчеркнуть, что в данном примере элементы Image и два Text-a будут отрисованы не с помощью View, а с помощью Drawable. Если мы можем не использовать сложные элементы Android UI Toolkit, если можно обойтись простыми и легковесными примитивами типа Drawable, то эффективнее использовать именно их. Если же какие-то элементы должны уметь реагировать, например, на клики, то они будут отрисованы с помощью View, чтобы переиспользовать всю непростую логику обработки UI-событий.



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

На рассмотренном примере мы познакомились с несколькими ключевыми элементами:

  • @LayoutSpec компоненты, комбинирующие другие компоненты. В итоге они превращаются в поддеревья в Internal Tree. Аналог кастомных ViewGroup.
  • Row и Column компоненты-контейнеры, служащие для задания расположения UI-элементов на экране. Это примитивы Flexbox основного Layout Engine в Litho. А Yoga это его кроссплатформенная реализация, которая используется не только в Litho, но также и в других библиотеках под Android, под iOS и в Web.
  • @MountSpec это те самые листовые ноды в Internal Tree Image, Text и другие. Это второй тип Spec, который описывает примитивные элементы, которые будут отрисованы на экране с помощью Drawable или View.

Как будет выглядеть код кастомной @MountSpec-и? Примерно так:

@MountSpecobject GradientSpec {  @OnCreateMountContent  fun onCreateMountContent(context: Context): GradientDrawable {      return GradientDrawable()  }  @OnMount  fun onMount(      c: ComponentContext,      drawable: GradientDrawable,      @Prop @ColorInt colors: IntArray) {      drawable.colors = colors  } }

В данном примере мы берем некоторый массив цветов и отрисовываем созданный на его основе градиент на экране. В андройде для работы с градиентами есть специальный GradientDrawable. Именно его мы и используем для рендеринга этого компонента. Инстанс данного типа нужно вернуть из специального lifecycle-метода, который помечается аннотацией @OnCreateMountContent и отвечает за создание контента для рендеринга.

Напомню, что компонент, описанный как MountSpec, может отрисовывать всего два типа контента: View или Drawable. В данном простом случае нам достаточно легковесного Drawable. Кроме операции создания контента мы также должны определить метод с аннотацией @OnMount для биндинга с данными перед тем, как компонент станет видимым. В нашем случае данными является тот массив цветов, который мы получаем на вход. Всё остальное Litho берет на себя и отрисовывает GradientDrawable c заданными цветами на экран. Для облегчения понимания можно сравнить методы, помеченные @OnCreateMountContent и @OnMount, с методами RecyclerView.Adapter onCreateViewHolder и onBindViewHolder соответственно.


Аннотация @MountSpec


В аннотации @MountSpec есть два параметра:

  1. poolSize параметр, который отвечает за то, сколько инстансов данного компонента будет создано заранее и помещено в пул, чтобы потом быстро использовать их при рендеринге интерфейса. По умолчанию этот параметр равен 3.
  2. isPureRender это булевый флаг, показывающий, что при пересоздании компонента с неизменными значениями Prop-параметров, результат его отрисовки всегда будет оставаться прежним. При обновлении UI-дерева компонентов это позволяет не пересоздавать и не перерисовывать такие чистые компоненты.

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

@MountSpec (poolSize = 30, isPureRender = true)class ImageSpec {  @ShouldUpdate  static boolean shouldUpdate(...) {}}public @interface MountSpec {  int poolSize() default 3;  boolean isPureRender() default false;}

У него очень большой poolSize (30). Cегодня типичная ситуация, когда приложение нагружено картинками, поэтому UI-компоненты для них лучше подготовить заранее в достаточном количестве. В то же время, если входной параметр Drawable, не меняется, то и вывод на экран такого компоненты тоже не поменяется, и, чтобы не делать лишних действий, можно установить флаг isPureRender В этом случае решение об обновлении компонента принимается на основании сравнения Prop-параметров с помощью equals(), если же вы хотите использовать кастомную логику сравнения, то её нужно поместить в функцию с аннотацией @ShouldUpdate.


Оптимизации в Litho


В Litho есть две ключевые оптимизации при построении Layout:

  1. Layout/Mount Diffing позволяет переиспользовать размеры (measurements) элементов с предыдущего рендеринга и переиспользовать LayoutOutput-ы (то, что выводится на экран) с предыдущего рендеринга.
  2. IncrementalMount позволяет превратить ваш UI в RecyclerView на стероидах без каких-либо дополнительных усилий.

Layout/Mount Diffing


Как это работает? При построении Internal Tree для нового компонента, также учитывается оставшееся с предыдущего рендеринга Diff Tree с готовыми размерами и LayoutOutput-ами всех узлов дерева.



Если входящие параметры для некоторого поддерева не изменились, то размеры и LayoutOutput для него просто копируются из Diff Tree в новый Internal Tree, минуя шаг измерения с помощью Yoga. Таким образом, LayoutState готов уже к концу построения Internal Tree.

IncrementalMount


Допустим, у вас есть своя социальная сеть с новостной лентой со сложными UI-элементами, например, постами с большим количеством, деталей. Не хотелось бы, чтобы при прокрутке экрана mount и отрисовка выполнялись для всего тяжелого UI поста, сразу как только первый пиксель покажется из-за края экрана. Если mount такого сложного элемента не уложится в 16мс, то мы будем видеть дерганый UI, особенно при быстрой прокрутке.
mount
Mount процесс получения контента для рендеринга (View или Drawable) и его добавление в текущую иерархию View

IncrementalMount в этом случае позволяет рендерить новостной пост не целиком, а постепенно, выполняя mount только для тех дочерних примитивных элементов, которые действительно попадают в видимую область экрана. А для тех же элементов, которые покидают её, выполнять unmount и возвращать их в пул, не дожидаясь, пока весь пост скроется за краем экрана, таким образом экономя память. Скролл существенно ускоряется за счёт того, что отрисовка тяжёлого поста разбивается на несколько фреймов. Всё это напоминает работу RecyclerView, но в Litho вам не надо как-то по-особому менять UI или использовать другие компоненты это работает из коробки.

Выводы на заметку:


Если вы определяете кастомную MountSpec-у, то:

  • можно использовать параметр isPureRender и метод @ShouldUpdate, чтобы не делать лишнюю работу при обновлении UI компонента.
  • зная, в каком объёме компонент будет использован в приложении, вы можете подготовить нужное количество инстансов заранее, настроив размер пула с помощью poolSize.


Управление Состоянием


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

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



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

Затем нам нужен метод для изменения стейта. Он помечается аннотацией @OnUpdateState и на вход принимает все тот же самый стейт, но не в виде неизменяемого значения, а завернутый в холдер StateValue, в котором стейт реально можно поменять.

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



В данном примере видно, что наборы параметров в описании метода и в месте его вызова не совпадают. Это происходит, потому что вы вызываете метод, определенный в Spec-классе не руками, а неявно, через сгенерированный метод в сгенерированном Component-классе, и все значения, необходимые для Spec-метода (в данном случае, StateValue), Litho подставит сам.

Каждое обновление стейта вызывает тот же эффект, что и передача новых значений Prop-параметров: снова нужно построить Internal Tree, получить LayoutState и отрисовать его на экран.

А что если у нас есть пара переменных для состояния и разные для них методы обновления? Допустим, у нас есть профиль супергероя, в котором нам надо поменять цвет и имя. Мы хотим сменить зеленого Халка на красного Железного Человека. У нас есть две переменные состояния color и name, и мы делаем два обновления путем присвоения переменных.

@OnUpdateStatefun changeColor(color: StateValue<Color>, @Param newColor: Color) {  color.set(newColor)}@OnUpdateStatefun changeName(name: StateValue<String>, @Param newName: String) {  name.set(newName)}...RootComponent.changeColor(c, Color.RED)RootComponent.changeName(c, "IronMan")

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

красного Халка не существует!
Поймали пасхалку? С меня стикер ;)



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

@OnUpdateStatefun changeHero(    color: StateValue<Color>, name: StateValue<String>,    @Param newColor: Color, @Param newName: String) {  color.set(newColor)  name.set(newName)}...RootComponent.changeHero(c, Color.RED, "IronMan")

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

Отложенное обновление состояния


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



Для шага заведем отдельный State-параметр step, в котором будем хранить текущее значение, и сделаем возможность вводить его с клавиатуры в поле TextInput. Так как при изменении этого значения в поле ввода новое число мы увидим сразу, то обновлять UI с новым значением step не надо, но запомнить его необходимо. Для этого надо выставить флаг canUpdateLazily, давая Litho понять, что этот State можно изменять без перестроения UI, лениво. В этом случае, помимо всех явно определенных @UpdateState методов, которые отвечают за обычные обновления состояния, сгенерируется ещё один метод lazyUpdateStep, выполняющий как раз такое ленивое обновление step. Префикс lazyUpdate общий для всех таких методов, а суффикс (Step) однозначно соответствует имени State-переменной.

@State(canUpdateLazily = true) step: IntRootComponent.lazyUpdateStep(c, value)

Выводы на заметку



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


Анимация в Litho


Давайте теперь перейдем от статического отображения UI к динамическому как в декларативном API Litho будет выглядеть описание анимации?



Рассмотрим простой пример (видео доклада 28:33-28:44) с кнопкой, которая меняет своё расположение при клике. Она прижимается то к правому, то к левому краю, но происходит это моментально, скачкообразно в этом случае пользователю не очевидно, что произошло.

Однако мы можем это исправить, добавить контекста и анимировать кнопку. Для этого надо сделать две вещи: надо пометить то, ЧТО мы анимируем, и описать, КАК надо анимировать. Мы двигаем кнопку, поэтому задаем ей свойство transitionKey.

Button.create(c)    .text(Catch me!)    .transitionKey(button)    .alignSelf(align)    .clickHandler(RootComponent.onClick(c))

Затем реализуем метод с аннотацией @OnCreateTransition, который создаёт описание анимации изменений между двумя отрисовками этого компонента, Transition между предыдущим и следующим состоянием UI. В данном случае Transition простой: мы создаём его с тем же transitionKey, которым пометили цель анимации (в данном случае, кнопку), и просим проанимировать только изменения горизонтальной координаты цели координаты X кнопки. В результате оно действительно анимируется (видео доклада 29:25-29.33).

@OnCreateTransitionfun onCreateTransition(c: ComponentContext): Transition {  return Transition.create("button")      .animate(AnimatedProperties.X)}

Такое описание анимации отлично, если вы четко знаете, что нужно анимировать и хотите полного контроля над тем, как это анимировать, но может стать многословным в сложных компонентах. Если же вы хотите проанимировать любые изменения в layout-е и сделать это автоматически, то в Litho есть специальный Transition.allLayout() для этих целей. Это нечто похожее на установку animateLayoutChanges = true для анимации всех изменений в нативной ViewGroup.

@OnCreateTransitionfun onCreateTransition(c: ComponentContext): Transition {  return Transition.allLayout()}

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

Чтобы самим определять, надо или нет анимировать конкретное изменение компонента, можно использовать Diff для Prop и State-параметров.

Diff это такой холдер, который содержит одновременно и текущее, и предыдущее значение для конкретного Prop/State, давая возможность их сравнить произвольным методом и сделать вывод, стоит анимировать или нет.



И если вернуть null из @OnCreateTransition, то анимироваться ничего не будет. Более того, будет пропущен весть этап подготовки анимации, что положительно скажется на производительности.

Обратите внимание, что и аннотации, и имена соответствующих Prop/State остаются такими же, как в @OnCreateLayout, меняется лишь тип с T на Diff<T>.

Выводы на заметку


Используйте Diff параметры для более тонкой настройки анимации изменения значений Prop и State.


Пошаговое внедрение


Вряд ли в существующем проекте кто-то решится в одночасье переписать весь UI на Litho. Поэтому возникаю логичные вопросы: можно ли осуществлять внедрение по частям? Могут ли Litho-компоненты жить бок о бок с нативным Android UI? И тут у меня для вас хорошие новости!

Да, ваш сложный UI можно портировать на Litho по частям:

  • С одной стороны можно использовать Litho-компоненты внутри существующего UI на View. Можно последовательно заменять сложные UI-поддеревья в вашей разметке на LithoView с аналогичной иерархией компонентов. Таким образом вы упростите изначальный UI и уменьшите глубину дерева элементов.
  • С другой стороны можно использовать кастомные View сложные графики, анимированные круговые диаграммы, видео-плееры, которые нелегко переписать на компоненты, в Litho-интерфейсах. Для этого View нужно обернуть в MountSpec-у (помните, что метод с @OnCreateMountContent может возвращать не только Drawable, но и View?), которую потом легко можно будет включать в иерархию компонентов.


Дебаггинг и тулы в Litho


А что же нам делать, если вдруг что-то не будет работать? Если будут вопросы, то где смотреть примеры? Как отладить интерфейс на Litho? Как быстро верстать и тюнить UI? Обо всём этом ниже.

Yoga playground


Litho использует концепцию и терминологию Flexbox для задания расположения элементов. Если вы с ней не знакомы, то для вас есть Yoga Playground. Это интерактивная песочница, где на схематичном представлении UI с виде прямоугольников можно поиграться с параметрами, подготовить макет вашего UI и даже экспортировать его в виде Litho-кода на Java.

Flipper + Litho plugins


Для Litho, к сожалению, нет поддержки в UI Preview. Стандартный Layout Inspector тоже не сможет показать иерархию компонентов Litho. Всё потому, что эти инструменты работают только с Android View. Но к счастью коллеги из команды UI Tools разрабатывают замечательный инструмент для разносторонней отладки мобильных приложений Flipper. Layout-плагин для Flipper умеет отображать иерархию UI-элементов интерфейса, который отображается на экране телефона, и распознаёт не только обычные View, но и компоненты Litho. Кроме того, при выделении какого-либо компонента, в боковой панели можно увидеть список свойств Props компонента, большую часть из которых можно менять в реальном времени и проверять изменения на устройстве. Это сильно упрощает финальную подстройку UI, во многом заменяя UI Preview.



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

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



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

Litho IntelliJ plugin


В Litho сильно отличающийся от стандартного подход к написанию UI, свои аннотации и lifecycle-методы много нового. Есть, конечно, документация, но чтобы при написании каждой новой Spec-и не обращаться к ней для уточнения любых вопросов, а стартовать быстро, наша команда также предоставляет IntelliJ / Android Studio плагин. Он добавляет шаблоны для создания LayoutSpec и MountSpec, шаблоны для генерации отдельных lifecycle-методов, а также возможность навигации между Spec-классом и сгенерированным по нему классом компонента.



Плагин можно установить через IntelliJ Plugin Marketplace.

Lithography Sample app


Ну а кроме всего вышеназванного, конечно же в репозитории есть sample-приложение Lithography. В нём можно посмотреть рецепты по реализации каких-то реальных примеров: создать UI карточки, загрузить картинку из интернета, реализовать Fast Scroll. Есть целые секции по работе со списками, различным способам анимации и так далее.

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


Резюме


Ключевые достоинства Litho в том, что обработку UI можно частично проводить на фоновом-потоке, его декларативный API позволяет проще описывать все возможные состояния UI, а при рендеринге получаются более плоские иерархии View. Несмотря на то, что мы все оборачиваем в Row, Column и прочие компоненты, на самом деле рисоваться будут только листовые элементы дерева и каждый пиксель как правило будет рисоваться по одному разу. Incremental Mount предоставляет возможность более гранулярного переиспользования отдельных атомарных MountSpec, а не только целых LayoutSpec компонентов элементов списка.


Бонус: Litho и Kotlin


С учётом завязки Litho на процессинг аннотаций и кодогенерацию, использование его с Kotlin может дать некоторое замедление сборки, так как KAPT печально известен своей неторопливостью. Ну и чего скрывать, для такого модного молодежного языка, как Kotlin, обилие аннотаций в API не выглядит очень удобно, когда везде правят разнообразные DSL-и. А хотелось бы вот как-то так просто создать UI в одной функции, да может даже прямо в Activity, и там же его в Activity и отрендерить, без плясок с LithoView:

class PlaygroundActivity : Activity() {  override fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    setContent {      val counter by useState { 1 }      Clickable(onClick = { updateState { counter.value = counter.value + 1 } }) {        Padding(all = 16.dp) {          Column {            +Text(text = "Hello, Kotlin World!", textSize = 20.sp)            +Text(                text = "with ${"".repeat(counter.value)} from London",                textStyle = Typeface.ITALIC)          }           }      }    }  }}

Так вот всё это реальный код! Пока что Kotlin API находится в активной разработке, но экспериментировать с ним можно уже сейчас Kotlin-артефакты выкладываются с каждым релизом Litho, а кроме того доступны их Snapshot-версии. Также, вы можете следить за развитием проекта на Github-е.

Настоятельно рекомендую ознакомиться с материалами по ссылкам:


Уже на следующей неделе состоится Mobius 2020 Piter. Там для Android-разработчиков тоже будет много интересного: например, выступит хорошо знакомый им Chet Haase из Google. Многие помнят его по выступлениям на Google I/O, а в этом году I/O отменили, но благодаря Mobius есть шанс всё равно увидеть Чета и даже лично задать ему вопрос.
Подробнее..

IOS in-app purchases Конфигурация и добавление в проект

22.06.2020 16:10:43 | Автор: admin

Всем привет, меня зовут Виталий, я основатель Adapty. Подписки один из способов монетизировать приложение. С их помощью вы можете дать пользователю возможность получить постоянный доступ к обновляемому контенту в приложении или же к предоставляемому сервису. В отличие от обычных покупок, где Apple берет себе 30% комиссию, на подписках эта комиссия сокращена до 15% в случае, если пользователь подписан в течение 1 года и более.Важныймомент: если пользователь отменит подписку, то данный счетчик сбросится через 60 дней.


В этой части мы научимся:


  • Создавать покупки в App Store Connect
  • Конфигурировать подписки указывать длительность, стоимость, пробные периоды
  • Получать список покупок в приложении

когда подключаешь покупки в приложении


Создание покупок


Перед тем, как начать внедрять покупки внутри приложения необходимо:


  • Оплатить Apple Developer аккаунт как физическое лицо или организация.
  • Приняты все соглашения в App Store Connect. Обновленные соглашения будут появляться сверху в личном кабинете App Store Connect, их легко заметить.

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


На странице нашего приложения в App Store Connect открываем вкладку In-App Purchases Manage. На этой вкладке отображается список созданных нами покупок. Для того, чтобы создать новую покупку, необходимо нажать на кнопку(+),которая находится около заголовка In-App Purchases.



Интерфейс создания покупок


Далее мы попадаем в диалог создания покупки. Наш выбор Auto-Renewable Subscription.



Выбираем 3 пункт


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


Назовем нашу группу Premium Access. При добавление следующей подписки интерфейс предложит добавить ее в уже существующую группу. Позже вы можете управлять группами в меню In-App Purchases Subscription Groups



Создание Группы подписок


Далее конфигурируем название подписки


  • Reference Name то, как будет отображаться подписка в App Store Connect, а также в разделе Sales и в отчетах
  • Product ID уникальныйидентификатор продукта, который используется в коде приложения

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



Добавим второй продукт точно так же, в итоге наш интерфейс вкладки In-App Purchases Manage будет выглядеть следующим образом:



Две подписки в приложении


Конфигурация подписок


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


Длительность и цены


Кликаем на продукт и конфигурируем его.



Здесь нам необходимо выбрать период(SubscriptionDuration). В нашем случае, выбираем 1 Month или 1 Year. После чего переходим в меню конфигурации цен. Можно гибко настраивать цены в зависимости от стран, но мы ограничимся автоматическими ценами, выбрав только цену в USD. App Store Connect автоматически переведет цены в другую валюту, не всегда ясно, как это происходит. Скорее всего для ваших целевых рынков вы захотите поменять цену руками.



Бесплатный пробный период (free trial)


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


Чтобы зайти в нужное нам меню, нажмите на кнопку(+)рядом с заголовком и выберите пунктCreate Introductory Offerиз выпадающего списка:



Выбираем список стран



Выбираем длительность оффера. Можно поставить No End Date, если не хотите себя ограничивать.



Последний этап выбор типа оффера. Как видно на следующем скриншоте, существует три типа:


  • Pay as you go использование со скидкой: пользователь платит сниженную цену в течение нескольких начальных периодов, а после становится обычным подписчиком со стандартными ценами.
  • Pay up front предоплата за использование приложения: пользователь сразу платит некоторую стоимость и получает возможность использовать приложение в течение определенного времени, а затем, также становится обычным подписчиком.
  • Free бесплатный пробный период, по истечение которого пользователь может стать подписчиком.

Нас интересует третий вариант, а продолжительность(Duration)устанавливаем на 1 неделю.



Сохраняем настройки.


Получение списка SKProduct


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


Хорошим правилом является создание класса-синглтона для работы со StoreKit. Такой класс имеет только один инстанс во всем приложении. МножествоproductIdentifiersбудет хранить в себе идентификаторы наших покупок:


import StoreKitclass Purchases: NSObject {    static let `default` = Purchases()    private let productIdentifiers = Set<String>(        arrayLiteral: "barcode_month_subscription", "barcode_year_subscription"    )    private var productRequest: SKProductsRequest?    func initialize() {        requestProducts()    }    private func requestProducts() {        // Will implement later    }}

Только идентификаторов недостаточно, чтобы полноценно пользоваться покупками необходимо получить: стоимость, валюту, локализацию, скидки. Возвращает всю эту и дальше большую информацию класс SKProduct. Чтобы получить эту информацию, нам необходимо сделать запрос к Apple. Создадим объектSKProductsRequest, назначим емуdelegate вметодыdelegateбудет приходить результат запроса. Вызываем методstart(), который инициализирует асинхронную процедуру:


private func requestProducts() {        productRequest?.cancel()        let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)        productRequest.delegate = self        productRequest.start()        self.productRequest = productRequest}

Если операция пройдет успешно, будет вызван методproductsRequest(didReceive response:), в котором и будет содержаться вся необходимая нам информация:


extension Purchases: SKProductsRequestDelegate {    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {        guard !response.products.isEmpty else {            print("Found 0 products")            return        }        for product in response.products {            print("Found product: \(product.productIdentifier)")        }    }    func request(_ request: SKRequest, didFailWithError error: Error) {        print("Failed to load products with error:\n \(error)")    }}

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


Found product: barcode_month_subscriptionFound product: barcode_year_subscription

При этом, если будет ошибка, то SKProduct с таким ID не вернется. Такое может быть, если продукт по какой то причине стал не валидный.


Ну вот и все, очередной забор за нашей спиной.


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

Подробнее..

Android и 3D камера. Распознавание лиц с защитой от Fraud

25.06.2020 18:18:39 | Автор: admin
Привет! Меня зовут Владимир Шальков, я Android-разработчик в Surf.

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



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

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

Недавно, класс FingerprintManager, который использовался для разблокировки приложений по отпечатку пальцев, задепрекейтили на API 28 и выше, и разработчикам предлагается использовать BiometricPrompt. Это класс, содержит логику, связанную с биометрией, в том числе по идентификации лиц. Однако использовать его в каждом смартфоне не получится, потому что согласно информации от Google, устройство должно иметь высокий рейтинг безопасности.

Некоторые устройства не имеют встроенного сканера отпечатка пальцев, от него отказались ввиду высокого уровня защиты от мошенничества при распознавании лица и всё благодаря фронтальному ToF(Time-of-flight) датчику. С его помощью можно построить карту глубины, тем самым увеличить устойчивость системы к взлому.

Требования


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

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

image

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

Ещё одно не менее важное ограничение работа в оффлайн режиме. Из-за этого мы не могли применять облачные сервисы для распознавания лиц. Кроме этого писать алгоритмы распознавания лиц с нуля неразумно, с учётом ограничения времени и трудозатрат. Возникает вопрос: зачем изобретать велосипед, если уже есть готовые решения? Исходя из всего выше сказанного, решили использовать библиотеку Face SDK от 3DiVi.

Получение изображения с камеры Intel RealSense


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

Чтобы начать работать с камерой Intel RealSense в Android-проекте, необходимо добавить зависимость RealSense SDK for Android OS: она является оберткой над официальной C++ библиотекой. В официальных семплах можно найти как произвести инициализацию и отобразить картинку с камер, на этом останавливаться не будем, там всё достаточно просто. Перейдём сразу к коду получения изображений:

private val pipeline = Pipeline()private val streamingHandler = Handler()private var streamRunnable: Runnable = object : Runnable {    override fun run() {        try {            FrameReleaser().use { fr ->                val frames = pipeline.waitForFrames(1000).releaseWith(fr)                val orgFrameSet = frames.releaseWith(fr)                val processedFrameSet = frames.applyFilter(align).releaseWith(fr)                val orgFrame: Frame = orgFrameSet.first(StreamType.COLOR, StreamFormat.RGB8).releaseWith(fr)                // Получаем фрейм цветного изображения                val videoFrame: VideoFrame = orgFrame.`as`(Extension.VIDEO_FRAME)                val processedDepth: Frame = processedFrameSet.first(StreamType.DEPTH, StreamFormat.Z16).releaseWith(fr)                // Получаем фрейм глубины изображения                val depthFrame: DepthFrame = processedDepth.`as`(Extension.DEPTH_FRAME)                upload(orgFrame) // Выводим на экран цветное изображение            }            streamingHandler.post(this)        } catch (e: Exception) {            Logger.d("Streaming, error: " + e.message)        }    }}streamingHandler.post(streamRunnable) // Запуск

С помощью FrameReleaser() мы получаем отдельные кадры с видеопотока, которые имеют тип Frame. К фреймам можно применять различные фильтры через applyFilter().

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

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

Преобразование фреймов в изображение


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

Формат изображений:

  • Цветное, с расширением .bmp, получаемое из VideoFrame
  • С картой глубины, имеющее расширение .tiff и получаемое из DepthFrame

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

fun videoFrameToMat(videoFrame: VideoFrame): Mat {    val colorMat = Mat(videoFrame.height, videoFrame.width, CvType.CV_8UC3)    val returnBuff = ByteArray(videoFrame.dataSize)    videoFrame.getData(returnBuff)    colorMat.put(0, 0, returnBuff)    val colorMatNew = Mat()    Imgproc.cvtColor(colorMat, colorMatNew, Imgproc.COLOR_RGB2BGR)    return colorMatNew}

Для сохранения цветного изображения необходимо сформировать матрицу с типом CvType.CV_8UC3, после конвертировать в BRG, чтобы цвета имели нормальный оттенок.
Используя метод Imgcodecs.imwrite, сохранить на устройстве:

fun VideoFrame.saveToFile(path: String): Boolean {    val colorMat = videoFrameToMat(this)    return Imgcodecs.imwrite(path + COLOR_IMAGE_FORMAT, colorMat)}

Тоже самое необходимо проделать для DepthFrame с тем лишь отличием, что матрица должна быть с типом CvType.CV_16UC1, так как изображение будет строиться из кадра, который содержит данные с датчика глубины:

fun depthFrameToMat(depthFrame: DepthFrame): Mat {    val depthMat = Mat(depthFrame.height, depthFrame.width, CvType.CV_16UC1)    val size = (depthMat.total() * depthMat.elemSize()).toInt()    val returnBuff = ByteArray(size)    depthFrame.getData(returnBuff)    val shorts = ShortArray(size / 2)    ByteBuffer.wrap(returnBuff).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(shorts)    depthMat.put(0, 0, shorts)    return depthMat}

Сохранение изображения с картой глубины:

fun DepthFrame.saveToFile(path: String): Boolean {    val depthMat = depthFrameToMat(this)    return Imgcodecs.imwrite(path + DEPTH_IMAGE_FORMAT, depthMat)}

Работа с библиотекой Face SDK


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

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

Далее, используя этот сервис, нужно создать объекты классов FacerecService.Config и Capturer:

private val service: FacerecService = FacerecService.createService(                dllPath,                confDirPath,                onlineLicenseDir        )private val confManual: FacerecService.Config = service.Config("manual_capturer.xml")private val capturerManual: Capturer = service.createCapturer(confManual)

Класс Capturer используется для распознавания лиц. Конфигурация manual_capturer.xml означает, что мы будем использовать алгоритмы из библиотеки OpenCV это детектор фронтальных лиц Viola-Jones, для распознавания используются признаки Хаара. Библиотека предоставляет готовое множество XML файлов с конфигурациями, отличающихся по характеристикам качества распознавания и времени работы. Менее быстрые методы имеют лучшие показатели по качеству распознавания. Если нам нужно распознавать лица в профиль, то следует использовать другой конфигурационный XML файл common_lprofile_capturer.xml. Конфигов достаточно много, с ними можно подробнее ознакомиться в документации. В нашем случае необходимо было использовать конфиг common_capturer4_singleface.xml это конфигурация с пониженным порогом качества в результате использования которой, всегда будет возвращаться не более одного лица.

Чтобы найти лицо на изображении применяется метод capturerSingleFace.capture(), в который передаётся массив байтов картинки, которая содержит лицо человека:

fun createRawSample(imagePath: String): RawSample? {    val imageColorFile = File(imagePath)    val originalColorByteArray = ImageUtil.readImage(imageColorFile)    return capturerSingleFace.capture(originalColorByteArray).getOrNull(0)}

Объект RawSample хранит информацию о найденном лице и содержит набор различных методов, например если вызвать getLandmarks(), то можно получить антропометрические точки лица.

Принадлежность лица реальному человеку


Чтобы определить реальный ли человек находится в кадре, а не фотография, приставленная к камере детекции лиц, библиотека Face SDK, предоставляет модуль DepthLivenessEstimator, он возвращает enum с одним из четырех значений:

  • NOT_ENOUGH_DATA слишком много отсутствующих значений на карте глубины
  • REAL наблюдаемое лицо принадлежит живому человеку
  • FAKE наблюдаемое лицо является фотографией
  • NOT_COMPUTED не удалось произвести вычисления

Инициализация модуля:

val depthLivenessEstimator: DepthLivenessEstimator = service.createDepthLivenessEstimator(           "depth_liveness_estimator_cnn.xml"   )

Определение принадлежности лица реальному человеку:

fun getLivenessState(            rgbPath: String,            depthPath: String    ): DepthLivenessEstimator.Liveness {        val imageColorFile = File(rgbPath + COLOR_IMAGE_FORMAT)        val originalColorByteArray = readImage(imageColorFile)        val originalRawSimple = capturerSingleFace.capture(originalColorByteArray).getOrNull(0)        val originalRawImage = RawImage(                SCREEN_RESOLUTION_WIDTH,                SCREEN_RESOLUTION_HEIGHT,                RawImage.Format.FORMAT_BGR,                originalColorByteArray        )        val originalDepthPtr = Natives().readDepthMap(depthPath + DEPTH_IMAGE_FORMAT)// параметры камеры        val hFov = 69.4f         val vFov = 42.5f         val depthMapRaw = DepthMapRaw()        with(depthMapRaw) {            depth_map_rows = originalRawImage.height            depth_map_cols = originalRawImage.width            depth_map_2_image_offset_x = 0f            depth_map_2_image_offset_y = 0f            depth_map_2_image_scale_x = 1f            depth_map_2_image_scale_y = 1f            horizontal_fov = hFov            vertical_fov = vFov            depth_unit_in_millimeters = 1f            depth_data_ptr = originalDepthPtr            depth_data_stride_in_bytes = (2 * originalRawImage.width)        }        return depthLivenessEstimator.estimateLiveness(originalRawSimple, depthMapRaw)}

Метод getLivenessState() в качестве параметров получает ссылки на изображения: цветное и с картой глубины. Из цветного мы формируем объект RawImage, этот класс предоставляет данные изображения в сыром виде и опциональной информации для обрезки. Из карты глубины формируется DepthMapRaw карта глубины, отрегистрированная в соответствии с исходным цветным изображением. Это необходимо сделать, чтобы использовать метод estimateLiveness(originalRawSimple, depthMapRaw), который вернёт нам enum с информацией реальное ли лицо было в кадре.

Стоит обратить внимание на формирование объекта DepthMapRaw. Одна из переменных имеет наименование depth_data_ptr это указатель на данные глубины, но как известно в Java нет указателей. Для получения указателя надо воспользоваться JNI функцией, которая в качестве аргумента принимает ссылку на изображение с картой глубины:

extern "C" JNIEXPORT jlong JNICALL Java_ru_face_detect_Natives_readDepthMap(JNIEnv *env, jobject obj, jstring jfilename){    const char * buf = env->GetStringUTFChars(jfilename, NULL);    std::string filename = buf;    env->ReleaseStringUTFChars(jfilename, buf);    cv::Mat depth_map = cv::imread(filename, -1);    unsigned char * data = new unsigned char[depth_map.rows * depth_map.cols * depth_map.elemSize()];    memcpy(data, depth_map.data, depth_map.rows * depth_map.cols * depth_map.elemSize());    return (jlong) data;}

Для вызова кода написанного на C в Kotlin, необходимо создать класс такого типа:

class Natives {    init {        System.loadLibrary("native-lib")    }    external fun readDepthMap(fileName: String): Long}

В System.loadLibrary() передаётся наименование файла .cpp, где содержится метод readDepthMap(), в нашем случае это native-lib.cpp. Также необходимо поставить модификатор external, который означает, что метод реализован не в Kotlin.

Идентификация лица


Не менее важная функция определение личности найденного лица в кадре. Face SDK позволяет реализовать это с помощью модуля Recognizer. Инициализация:

val recognizer: Recognizer = service.createRecognizer(        "method8v7_recognizer.xml",        true,        true,        true)

Мы используем конфигурационный файл method8v7_recognizer.xml, который имеет самую высокую скорость распознавания, но при этом качество распознавания ниже, чем у методов 6v7 и 7v7.

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

var templates = Vector<Template>()val rawSample = createRawSample(imageUrl)val template = recognizer.processing(rawSample)templates.add(template)

Для создания Template используется метод recognizer.processing(), в качестве параметра передаётся RawSample. После того, как список с шаблонами лиц сформирован, его необходимо добавить в Recognizer и сохранить полученный TemplatesIndex, который нужен для быстрого поиска в больших базах:

val templatesIndex = recognizer.createIndex(templates, SEARCH_THREAD_COUNT)

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

fun detectFaceSearchResult(rgbPath: String): Recognizer.SearchResult {    val rawSample = createRawSample(rgbPath + COLOR_IMAGE_FORMAT)    val template = recognizer.processing(rawSample)    val searchResult = recognizer.search(            template,            templateIndex,            searchResultCount,            Recognizer.SearchAccelerationType.SEARCH_ACCELERATION_1    ).firstElement()    return searchResult}

Функция recognizer.search() вернёт нам результат, где мы можем получить индекс найденного элемента, сопоставить его со списком лиц из базы данных и идентифицировать персону. Кроме этого, мы можем узнать величину сходства, действительное число от 0 до 1. Данная информация предоставлена в классе Recognizer.MatchResult, переменная scope:

val detectResult = detectFaceSearchResult(rgbPath)// Величина сходства шаблонов - действительное число от 0 до 1.val scoreResult = detectResult.matchResult.score

Заключение


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

В Android SDK, постепенно добавляется API, который позволяет разработчику работать с системой идентификации лиц, однако сейчас всё находится на начальном этапе развития. А если говорить о системе контроля доступа с использованием планшета на Android, библиотеки Face SDK и 3D камеры Intel RealSense, хочется отметить большую гибкость и расширяемость. Нет привязки к устройству, камеру можно подключить к любому современному смартфону. Можно расширить линейку поддерживаемых 3D камер, а также подключить несколько штук к одному устройству. Есть возможность адаптировать написанное приложение под Android Things, и использовать его в своем умном доме. Если посмотреть на возможности библиотеки Face SDK, то с её помощью мы можем добавить идентификацию лиц в непрерывном видеопотоке, определять пол, возраст и эмоции. Эти возможности дают простор для множества экспериментов. А мы на своём опыте можем сказать: не бойтесь экспериментов и бросайте вызов себе!
Подробнее..

Как смотреть WWDC 2020, если ты не разработчик

30.06.2020 14:13:12 | Автор: admin

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


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



Даб-даб ди си


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


У большинства людей конференция ассоциируется с Keynote, на которой Apple рассказывает о программных новинках, для кого-то ещё и с Apple Design Awards, где награждают лучшие приложения по версии экспертов эппла. Но самое интересное начинается дальше. В течении одной недели абсолютно закрытая компания Apple приоткрывает завесу тайны над разработками и даёт пообщаться с инженерами и сотрудниками. Для этого проводится около 100 сессий, на которых инженеры рассказывают про различные аспекты связанные с анонсированными новинками и про то, как правильно разрабатывать свои продукты с их учётом. Если сессий недостаточно или есть вопросы, то на лабах можно задавать инженерам любые вопросы, связанные со своими проектами или с только анонсированными технологиями. Также проходит много событий, концертов встреч и подкастов вживую.


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


Но если раньше для этого нужно было испытать удачу и получить билет, прилететь в США, то в 2020 конференция стала ближе как никогда. И Apple подошли к этому основательно.


keynote_epic


Только посмотрите Keynote и Platform state of the Union, который из стандартной презентации со сцены и сменой ведущих сменился в шоу с эпичными переходами. Остальные сессии хоть и не такие эпичные, но стали заметно живее и теперь лучше смотрятся онлайн.


Где смотреть?


Но для начала определимся как смотреть.
В этом году Apple неплохо обновили своё приложение для разработчиков и добавили возможность просматривать сессии прямо в нём. Если по каким-то причинам официальное приложение не подходит, то все ещё актуально приложение WWDC для macOS.


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


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


  • На сайте https://asciiwwdc.com собираются текстовые версии докладов. (Транскрипты обновляются обычно в течении месяца после завершения очередной WWDC)
  • Сообщество делится конспектами сессий на GitHub, например https://github.com/Blackjacx/WWDC и https://wwdcnotes.com

Что смотреть?


Чтобы не привязываться к конкретным ролям пройдёмся по основным этапам жизни любой фичи проекта:


  • Идея и гипотезы.
  • Проектирование и прототипирование.
  • Разработка и контроль качества.
  • Бета тестирования и релиз.

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





Идея / гипотеза


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



Проектирование и планирование


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


Всем кто отвечает и за эту стадию работы над проектом рекомендуем посмотреть:



Также все материалы про дизайн уже с любовью собраны Apple:



Разработка и контроль качества


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


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


Сессии про тестирование собраны, как и сессии для дизайнеров, в отдельный раздел и в подборке The suite life of testing.


Бета тестирования и релиз


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


Также на этой WWDC особое внимание было уделено работе Entreprise. Под это направление отвели отдельную подборку и анонсировали разные интересный фичи в iOS. Чего только стоит Local Push Connectivity, благодаря которому уведомления можно будет рассылать в рамках локальной сети.


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


Наш топ сессий на эти тему тестирования:



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


Today@WWDC



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



А что, если нужно поддерживать старые iOS?


Это отличный повод пересмотреть сессии с WWDC 2-х летней давности.


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


  • Гайды по просмотру WWDC 2016-2019 от UseYourLoaf.
  • Подборка нововведений WWDC 2019 от Патрика Балестры.

Как смотреть?


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


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


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


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


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


Пример обзора
Пример обзора


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


Часть результатов прошлогоднего брейншторма
Часть результатов прошлогоднего брейншторма


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


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


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


Желаем приятного просмотра!

Подробнее..

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

02.07.2020 18:23:23 | Автор: admin


Всем привет! Меня зовут Михаил Булгаков (нет, не родственник), я работаю релиз-инженером в Badoo. Пять лет назад я занялся автоматизацией релизов iOS-приложений, о чём подробно рассказывал в этой статье. А после взялся и за Android-приложения.

Сегодня я подведу некоторые итоги: расскажу, к чему мы пришли за это время. Long story short: любой причастный к процессу сотрудник может зарелизить хоть все наши приложения на обеих платформах в несколько кликов без головной боли, больших затрат времени, регистрации и СМС. Так, наш отдел релиз-инженеров за 2019 год сэкономил около 830 часов.

За подробностями добро пожаловать под кат!

Что стоит за мобильным релизом


Выпуск приложения в Badoo состоит из трёх этапов:

  1. Разработка.
  2. Подготовка витрины в магазине приложений: тексты, картинки всё то, что видит пользователь в App Store или Google Play.
  3. Релиз, которым занимается команда релиз-инжиниринга.

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

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

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

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

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



Первые шаги на пути к автоматизации: загрузка метаданных


Как это работало в самом начале: для каждого релиза создавалась таблица в Google Sheets, в которую продакт-менеджер заливал выверенный мастер-текст на английском, после чего переводчики адаптировали его под конкретную страну, диалект и аудиторию, а затем релиз-инженер переносил всю информацию из этой таблицы в App Store или Google Play.

Первый шаг к автоматизации, который мы сделали, интегрировали перевод текстов в наш общий процесс переводов. Останавливаться на этом не буду это отдельная большая система, про которую можно прочитать в нашей недавней статье. Основной смысл в том, что переводчики не тратят время на таблички и работают с интерфейсом для удобной загрузки руками (читай: ctrl+c ctrl+v) переведённых вариантов в стор. Кроме того, присутствуют задатки версионирования и фундамент для Infrastructure-as-Code.

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

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


Наша реальность по состоянию на 2015 год

В среднем на релиз одного приложения при наличии актуальной версии скриншотов уходило около полутора-двух часов работы релиз-инженера в случае с iOS и около получаса в случае с Android. Разница обусловлена тем, что iOS-приложения должны пройти так называемый Processing, который занимает некоторое время (отправить приложение на Review до успешного завершения Processing невозможно). Кроме того, App Store сам по себе по большинству операций в тот момент работал гораздо медленнее, чем Google Play.

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

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

Коротко о Fastlane


Сегодня Fastlane это продукт, который способен практически полностью автоматизировать все действия от момента окончания разработки до релиза приложения в App Store и Google Play. И речь не только о загрузке текстов, скриншотов и самого приложения здесь и управление сертификатами, и бета-тестирование, и подписывание кода, и многое другое.

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

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

Со временем мы внедрили большинство предоставляемых Fastlane возможностей в системы сборки, подписания, заливки и т. д. наших приложений. И несказанно этому рады. Зачем изобретать колесо, да ещё и поддерживать его правильную форму, когда можно один раз написать унифицированный сценарий, который будет сам крутиться в CI/CD-системе?

Автоматизация iOS-релизов


По причине того, что Google Play более дружелюбен к разработчикам, на релиз Android-приложения уходило очень мало времени: без обновления текстов, видео и скриншотов пара минут. Отсюда и отсутствие необходимости в автоматизации. А вот с App Store проблема была очень даже осязаемой: слишком много времени уходило на отправку приложений на Review. Поэтому было решено начать автоматизацию именно с iOS.

Подобие своей системы автоматизации взаимодействия с App Store мы обдумывали (и даже сделали прототипы), но у нас не было ресурсов на допиливание и актуализацию. Также не было никакого мало-мальски адекватного API от Apple. Ну и последний гвоздь в гроб нашего кастомного решения вбили регулярные обновления App Store и его механизмов. В общем, мы решили попробовать Fastlane тогда ещё версии 2015 года.

Первым делом был написан механизм выгрузки переведённых текстов для приложений в нужную структуру как компонент нашей общей внутренней системы AIDA (Automated Interactive Deploy Assistant). Эта система своеобразный хаб, связующее звено между всеми системами, технологиями и компонентами, используемыми в Badoo. Работает она на самописной системе очередей, реализованной на Golang и MySQL. Поддерживает и совершенствует её в основном отдел Release Engineering. Подробнее о ней мы рассказывали в статье ещё в 2013 году, с тех пор многое изменилось. Обещаем рассказать про неё снова AIDA классная!

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

Это сократило время подготовки релиза с пары часов до примерно 30 минут, из которых только полторы минуты надо было что-то делать руками! Остальное время ждать. Ждать окончания Processing. Механизм стал прорывом на тот момент как раз потому, что почти полностью избавил нас от ручной работы при подготовке AppStore к релизу. Под скрипт мы сделали репозиторий, к которому дали доступ людям, имеющим непосредственное отношение к релизам (проджект-менеджерам, релиз-инженерам).

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

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

  1. Нужно было идти в TeamCity за свежей сборкой, скачивать оттуда IPA-файл, загружать его в App Store через Application Manager.
  2. Потом идти в интерфейс с переводами в AIDA, смотреть, готовы ли все переводы, запускать скрипт, убеждаться, что он правильно сработал (всё-таки на тот момент Fastlane был ещё сыроват).
  3. После этого залезать в App Store и обновлять страницу с версией до того момента, пока не завершится Processing.
  4. И только после этого отправлять приложение на Review.

И так с каждым приложением. Напомню, на тот момент у нас их было восемь.

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

TestFlight это приложение сторонних разработчиков, когда-то купленное Apple для тестирования готового приложения внешними тестировщиками практически в продакшен-окружении, то есть с push-оповещениями и вот этим всем.


AIDA молодец, будь как AIDA!

Всё это привело к сокращению времени с получаса до полутора минут на всё про всё: IPA-файл успевал пройти Processing ещё до того момента, когда команда QA-инженеров давала отмашку на запуск релиза. Тем не менее нам всё равно приходилось идти в App Store, выбирать нужную версию и отправлять её на Review.

Плюс, был нарисован простенький интерфейс: мы же все любим клац-клац.


Вот так, вкладка за вкладкой, Ctrl+C Ctrl+V...

Автоматизация Android-релизов


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

  1. Заходить в консоль Google Play, чтобы убедиться, что предыдущая версия раскатана на 100% пользователей или заморожена.
  2. Создавать новую версию релиза с обновлёнными текстами и скриншотами (при наличии).
  3. Загружать APK-файл (Android Package), загружать Mapping-файл.
  4. Идти в HockeyApp (использовался в то время для логирования крашей), загружать туда APK-файл и Mapping-файл.
  5. Идти в чат и отписываться о статусе релиза.

И так с каждым приложением.

Да, у Google Play есть свой API. Но зачем делать обёртку, следить за изменениями в протоколе, поддерживать её и плодить сущности без необходимости, если мы уже используем Fastlane для iOS-релизов? К тому же он комфортно существует на нашем сервере, варится в своём соку и вообще обновляется. А к тому времени он ещё и научился адекватно релизить Android-приложения. Звёзды сошлись!

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

Заливкой APK- и Mapping-файлов в Google Play занимался Fastlane. Надо сказать, что по проторенной тропе идти гораздо проще: реализовано это было достаточно быстро с минимальным количеством усилий.

На определённом этапе реализации автоматизации случился переход с APK-архивов на AAB (Android App Bundle). Опять же, нам повезло, что по горячим следам довольно быстро получилось всё поправить, но и развлечений добавилось в связи с этим переходом. Например, подгадил HockeyApp, который не умел использовать AAB-архивы в связи с подготовкой к самовыпиливанию. Так что для того чтобы комфортно продолжать его использовать, нужно было после сборки AAB разобрать собранный архив, доставать оттуда Mapping-файл, который полетит в HockeyApp, а из AAB нужно было отдельно собрать APK-файл и только потом загружать его в тот же HockeyApp. Звучит весело. При этом сам Google Play отлично раскладывает AAB, достаёт оттуда Mapping-файл и вставляет его куда нужно. Так что мы избавились от одного шага и добавили несколько, но от этого было никуда не деться.

Был написан интерфейс (опять же, по аналогии с iOS), который умел загружать новую версию, проверять релиз вдоль и поперёк, управлять текущим активным релизом (например, повышать rollout percentage). В таком виде мы отдали его ответственным за релизы членам команды Android QA, стали собирать фидбэк, исправлять недочёты, допиливать логику (и что там ещё бывает после релиза 1.0?).

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

Унификация флоу мобильных релизов


К моменту автоматизации Android-релизов Fastlane наконец-то научился отправлять версии iOS-приложений на ревью. А мы немного усовершенствовали систему проверки версий в AIDA.

Пришла пора отдать iOS-релизы на откуп команде QA-инженеров. Для этого мы решили нарисовать красивую формочку, которая бы полностью покрывала потребности, возникающие в процессе релиза iOS-приложений: давала бы возможность выбирать нужный билд в TeamCity по предопределённым параметрам, выбирать вариант загружаемых текстов, обновлять или нет опциональные поля (например, Promotional Text).

Сказано сделано. Формочка получилась очень симпатичная и полностью удовлетворяет все запросы. Более того, с её внедрением появилась возможность выбирать сразу все необходимые приложения со всеми требуемыми параметрами, так что и взаимодействие с интерфейсом свелось к минимуму. AIDA по команде присылает ссылку на build log, по которому можно отслеживать возникающие ошибки, убеждаться, что всё прошло хорошо, получать какую-то debug-информацию вроде версии загружаемого IPA-файла, версии релиза и т. д. Вот так красиво iOS-релизы и были переданы команде iOS QA.


Ну симпатично же?

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

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

Вывод


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

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

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

Пять лет. Почему так долго? Во-первых, мобильные релизы далеко не единственная зона ответственности нашей небольшой команды. Во-вторых, конечно же, требовалось время на развитие нового open-source-проекта Fastlane; наша система релизов развивалась вместе с ним.

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

Бюджетный DI на антипаттернах

03.07.2020 08:04:12 | Автор: admin

image


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


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


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


Введение


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


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


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


Хорошее содержание



Принципы


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


  1. Не выпендривайся. Тупой и понятный код в большинстве случаев лучше умного и непонятного.
  2. Будь краток. Кода должно быть настолько мало, чтобы его не жалко было в любой момент выкинуть и написать заново за один день.
  3. Удобство превыше правил. Если можно облегчить себе жизнь, пожертвовав принципами SOLID, пожертвуй принципами SOLID.
  4. Получай удовольствие. Если есть разные варианты решения проблемы, выбирай более веселый.

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


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


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


У меня нет намерения подробно объяснять, что такое DI-контейнер. Про это классно рассказывают в двух статьях из репозитория Ninject: раз, два (уберите от экрана детей, там код на С#). Еще есть небольшое объяснение в репозитории самого популярного DI-контейнера под iOS Swinject (заметили, что Swinject это Ninject на Swift?). Хардкорщикам могу предложить статью Фаулера от 2004 года.


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


Решение


Существует несколько довольно популярных реализаций DI-контейнеров под iOS (Swinject, Cleanse, Dip, DITranquility, EasyDI), но использовать чужую реализацию, согласитесь, скучно. Гораздо веселее использовать мою.


Готовы немного развлечься и написать DI-контейнер с нуля? Похожую реализацию мне показал однажды один из самых крутых iOS-разработчиков, простой сибирский парень teanet, за что ему огромное спасибо. Я ее немного переосмыслил и готов поделиться с вами. Начнем с протокола IContainer:


protocol IContainer: AnyObject {    func resolve<T: IResolvable>(args: T.Arguments) -> T}

Привычка из прошлой жизни я всегда пишу I перед протоколами. Буква I значит interface. У нашего интерфейса протокола всего один метод resolve(args:), который от нас принимает какие-то аргументы T.Arguments, а взамен возвращает экземпляр типа T. Как видно, не любая сущность может быть Т. Чтобы стать полноправным T, нужно реализовать IResolvable. IResolvable это еще один протокол, о чем нам услужливо подсказывает буква I в начале имени. Он выглядит вот так:


protocol IResolvable: AnyObject {    associatedtype Arguments    static var instanceScope: InstanceScope { get }    init(container: IContainer, args: Arguments)}

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


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


Свойство instanceScope отвечает за область видимости, в которой будет существовать экземпляр объекта:


enum InstanceScope {    case perRequst    case singleton}

Это довольно стандартная для DI-контейнеров штуковина. Значение perRequest означает, что для каждого вызова resolve(args:) будет создан новый экземпляр T. Значение singleton означает, что экземпляр T будет создан единожды при первом вызове resolve(args:). При последующих вызовах resolve(args:) в случае singleton будет отдаваться закэшированная копия.


С протоколами разобрались, приступаем к реализации:


class Container {    private var singletons: [ObjectIdentifier: AnyObject] = [:]    func makeInstance<T: IResolvable>(args: T.Arguments) -> T {        return T(container: self, args: args)    }}

Тут ничего особенного: кэш синглтонов будем хранить в виде словаря singletons. Ключом словаря нам послужит ObjectIdentifier это стандартный тип, поддерживающий Hashable и представляющий собой уникальный идентификатор объекта ссылочного типа (через него, кстати, реализован оператор === в Swift). Метод makeInstance(args:) умеет на лету создавать любые экземпляры T благодаря тому, что мы обязали все T реализовать один и тот же инициализатор.


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


extension Container: IContainer {    func resolve<T: IResolvable>(args: T.Arguments) -> T {        switch T.instanceScope {        case .perRequst:            return makeInstance(args: args)        case .singleton:            let key = ObjectIdentifier(T.self)            if let cached = singletons[key], let instance = cached as? T {                return instance            } else {                let instance: T = makeInstance(args: args)                singletons[key] = instance                return instance            }        }    }}

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


Вот, собственно, и все. Мы только что написали свой DI-контейнер в 50 строк кода. Но как этой штукой вообще пользоваться? Да очень просто.


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


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


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


Полезный экстеншен номер раз:


protocol ISingleton: IResolvable where Arguments == Void { }extension ISingleton {    static var instanceScope: InstanceScope {        return .singleton    }}

И второй такой же, но другой:


protocol IPerRequest: IResolvable { }extension IPerRequest {    static var instanceScope: InstanceScope {        return .perRequst    }}

Теперь вместо IResolvable можно конформить более лаконичным ISingleton/IPerRequest и сэкономить тем самым несколько секунд жизни, потратив их на саморазвитие. А вот и реализация OrdersProvider подъехала:


class OrdersProvider: ISingleton {    required init(container: IContainer, args: Void) { }    func loadOrders(for customerId: Int, date: Date) {        print("Loading orders for customer '\(customerId)', date '\(date)'")    }}

Мы предоставили required init, как того требует протокол, но, так как OrdersProvider ни от чего не зависит, этот инициализатор у нас пустой. Каждый раз, когда мы будем доставать OrdersProvider из контейнера, мы будем получать один и тот же экземпляр, потому что такова дефолтная реализация instanceScope для ISingleton.


А вот и модель представления собственной персоной:


final class OrdersVM: IPerRequest {    struct Args {        let customerId: Int        let date: Date    }    private let ordersProvider: OrdersProvider    private let args: Args    required init(container: IContainer, args: Args) {        self.ordersProvider = container.resolve()        self.args = args    }    func loadOrders() {        ordersProvider.loadOrders(for: args.customerId, date: args.date)    }}

Эта вью-модель не может существовать без аргументов OrdersVM.Args, которые мы получаем через required init. В этот инициализатор также попадает сам контейнер, из которого мы без лишней суеты извлекаем экземпляр OrdersProvider посредством вызова resolve().


Вызов метода loadOrders() использует ordersProvider для загрузки заказов, предоставляя ему необходимые для работы аргументы. Каждый раз, когда мы будем доставать OrdersVM из контейнера, мы будем получать новый экземпляр, потому что такова дефолтная реализация instanceScope для IPerRequest.


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


let container = Container()let viewModel: OrdersVM = container.resolve(args: .init(customerId: 42, date: Date()))viewModel.loadOrders()

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


Loading orders for customer '42', date '2020-04-22 17:41:49 +0000'

Критика


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


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


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


final class OrdersVM {    private let ordersProvider: IOrdersProvider    init(ordersProvider: IOrdersProvider) {       self.ordersProvider = ordersProvider    }}

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


final class OrdersVM {    private let ordersProvider: IOrdersProvider    init() {        self.ordersProvider = ServiceLocator.shared.resolve()    }}

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


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


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


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


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


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


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


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


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


Вот вам смысл этого многословного раздела в виде двух списков.


Короче, минусы


  • Зависимости достаем в конструкторе прямо из контейнера (Service Locator).
  • Не получится закрыть зависимость протоколом (принцип на букву D).

Короче, плюсы


  • Простая и лаконичная реализация (50 строк кода).
  • Не надо регистрировать зависимости (вообще не надо).
  • Извлечение из контейнера никогда не сломается (совсем никогда).
  • Нельзя передать невалидные аргументы (не скомпилируется).

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


One More Thing: автоматическое внедрение зависимостей через обертки свойств


В 2019 году в компании Apple придумали инкапсулировать повторяющуюся логику гетеров и сетеров в переиспользуемые атрибуты и назвали это обертками свойств (property wrappers). С помощью таких оберток ваши свойства волшебным образом могут получить новое поведение: запись значения в Keychain или UserDefaults, потокобезопасность, валидацию, логирование да много чего.


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


Чтобы написать свою обертку свойства в минимальной комплектации, нужно создать класс или структуру, предоставить свойство wrappedValue и пометить все это дело атрибутом @propertyWrapper:


@propertyWrapperstruct Resolvable<T: IResolvable> where T.Arguments == Void {    private var cache: T?    var wrappedValue: T {        mutating get {            if let cache = cache {                return cache            }            let resolved: T = ContainerHolder.container.resolve()            cache = resolved            return resolved        }    }}

Из этого незамысловатого кода мы видим, что наш property wrapper называется Resolvable. Он работает со всеми типами Т, которые реализуют одноименный протокол и не требуют аргументов при инициализации.


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


final class ContainerHolder {    static var container: IContainer!}

Имея в своем арсенале обертку Resolvable<T>, мы можем применить ее к какой-нибудь зависимости, например к ordersProvider:


@Resolvableprivate var ordersProvider: OrdersProvider

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


private var _ordersProvider = Resolvable<OrdersProvider>()var ordersProvider: OrdersProvider {  get { return _ordersProvider.wrappedValue }}

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


Теперь знакомая нам модель представления может позволить себе не извлекать из контейнера OrdersProvider в инициализаторе, а просто пометить соответствующее свойство атрибутом @Resolvable. Вот так:


final class OrdersVM: IPerRequest {    struct Args {        let customerId: Int        let date: Date    }    @Resolvable    private var ordersProvider: OrdersProvider    private let args: Args    required init(container: IContainer, args: Args) {        self.args = args    }    func loadOrders() {        ordersProvider.loadOrders(for: args.customerId, date: args.date)    }}

Самое время собрать все вместе и порадоваться, что все работает как прежде:


ContainerHolder.container = Container()let viewModel: OrdersVM = ContainerHolder.container.resolve(    args: .init(customerId: 42, date: Date()))viewModel.loadOrders()

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


Loading orders for customer '42', date '2020-04-23 18:47:36 +0000'



Unit-тесты, раздел под звездочкой


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


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


protocol IOrdersProvider {    func loadOrders(for customerId: Int, date: Date)}extension OrdersProvider: IOrdersProvider {}

Теперь во вью-модели можем сделать второй инициализатор, который будет принимать этот протокол:


final class OrdersVM: IPerRequest {    struct Args {        let customerId: Int        let date: Date    }    private let ordersProvider: IOrdersProvider    private let args: Args    required convenience init(container: IContainer, args: Args) {        self.init(            ordersProvider: container.resolve() as OrdersProvider,            args: args)    }    init(ordersProvider: IOrdersProvider, args: Args) {        self.args = args        self.ordersProvider = ordersProvider    }    func loadOrders() {        ordersProvider.loadOrders(for: args.customerId, date: args.date)    }}

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


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


Забегая вперед, скажу, что далее от нас потребуется хранить объекты IResolvable в некоторой коллекции. Однако если мы попробуем сделать это, то столкнемся с суровой действительностью в виде ошибки, до боли знакомой каждому iOS-разработчику: protocol 'IResolvable' can only be used as a generic constraint because it has Self or associated type requirements. Типичный способ как-то справиться с этой ситуацией налить себе чего-нибудь покрепче и применить механизм с пугающим названием стирание типов (type erasure).


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


struct AnyResolvable {    private let factory: (IContainer, Any) -> Any?    init<T: IResolvable>(resolvable: T.Type) {        self.factory = { container, args in            guard let args = args as? T.Arguments else { return nil }            return T(container: container, args: args)        }    }    func resolve(container: IContainer, args: Any) -> Any? {        return factory(container, args)    }}

Кода здесь немного, но он хитрый. В инициализатор мы принимаем настоящий живой тип T, который не можем никуда сохранить. Вместо этого мы сохраняем замыкание, обученное создавать экземпляры этого типа. Замыкание впоследствии используется по своему прямому назначению в методе resolve(container:args:), который понадобится нам позже.


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


final class ContainerMock: Container {    private var substitutions: [ObjectIdentifier: AnyResolvable] = [:]    public func replace<Type: IResolvable, SubstitutionType: IResolvable>(        _ type: Type.Type, with substitution: SubstitutionType.Type) {        let key = ObjectIdentifier(type)        substitutions[key] = AnyResolvable(resolvable: substitution)    }    override func makeInstance<T: IResolvable>(args: T.Arguments) -> T {        return makeSubstitution(args: args) ?? super.makeInstance(args: args)    }    private func makeSubstitution<T: IResolvable>(args: T.Arguments) -> T? {        let key = ObjectIdentifier(T.self)        let substitution = substitutions[key]        let instance = substitution?.resolve(container: self, args: args)        return instance as? T    }}

Давайте разбираться.


Класс ContainerMock наследуется от обычного Container, переопределяя метод makeInstance(args:), используемый контейнером для создания сущностей. Новая реализация пытается создать подставную зависимость вместо настоящей. Если ей это не удается, она печально разводит руками и фолбечится на реализацию базового класса.


Метод replace(_:with:) позволяет сконфигурировать моковый контейнер, указав тип зависимости и соответствующий ей тип мока. Эта информация хранится в словаре substitutions, который использует уже знакомый нам ObjectIdentifier для ключа и AnyResolvable для хранения типа мока.


Для создания моков используется метод makeInstance(args:), который по ключу пытается достать нужный AnyResolvable из словаря substitutions и создать соответствующий экземпляр с помощью метода resolve(container:args:).


Использовать все это дело мы будем следующим образом. Создаем моковый OrdersProvider, переопределяя метод loadOrders(for:date:):


final class OrdersProviderMock: OrdersProvider {    override func loadOrders(for customerId: Int, date: Date) {        print("Loading mock orders for customer '\(customerId)', date '\(date)'")    }}

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


let container = ContainerMock()container.replace(OrdersProvider.self, with: OrdersProviderMock.self)let viewModel: OrdersVM = container.resolve(args: .init(customerId: 42, date: Date()))viewModel.loadOrders()

Для справки, этот код производит следующий консольный вывод:


Loading mock orders for customer '42', date '2020-04-24 17:47:40 +0000'

Заключение


Сегодня мы вероломно поступились принципом инверсии зависимостей и в очередной раз изобрели велосипед, реализовав бюджетный DI с помощью анти-паттерна Service Locator. Попутно мы познакомились с парой полезных техник iOS-разработки, таких как type erasure и property wrappers, и не забыли про unit-тесты.


Автор не рекомендует использовать код из этой статьи в приложении для управления ядерным реактором, но если у вас небольшой проект и вы не боитесь экспериментировать свайп вправо, its a match <3




Весь код из этой статьи можно скачать в виде Swift Playground.

Подробнее..

NewNode децентрализованная CDN от разработчика FireChat

03.07.2020 14:20:11 | Автор: admin


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


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

dCDN


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

Протокол


Далее выясняется, что NewNode это peer-to-peer протокол, на котором уже строится dCDN. Он обещает высокую скорость, что обычно вызывает проблемы у децентрализованных сетей. Формально протокол нигде не описан, но из пдфки можно понять, что работает он использует:
  • LEDBAT
  • Bittorrent DHT
  • Соединения device-to-device из FireChat

Отдельным пунктом указано свойство сетей на NewNode разворачиваться и чиниться автоматически (последнее, скорее всего, подразумевает нестабильность mesh-сети из мобильных устройств). Также, поскольку разработчики надеются внедрить поддержку протокола во все возможные приложения, трафик, генерируемый NewNode'ом не будет демаскировать пользователя. Заявлена защита от DDoS и отдельно выделена фраза:
Take advantage of BitTorrents 250 Million user base


Вообще непонятно, что этим хотели сказать и как обращение к Bittorrent DHT в протоколе приравняли к юзербазе Bittorrent'а.

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

Репозиторий


В нём лежат SDK под Android, iOS и macOS/Linux. За три с половиной года существования проекта в нём отметились 4 контрибьютора, но по сути весь код написан одним разработчиком Greg Hazel. Тут я, конечно, приуныл вся эта амбициозная мишура оказалась по сути пет-проектом одного разработчика. Но кое-что обнадёживает меня.



Отдельные связи стали выстраиваться ещё на сайте, а порывшись в гитхабе, я вспомнил окончательно. CEO Clostra, разрабатывающей проект, и один из контрибьюторов Станислав Шалунов, один из разработчиков FireChat и автор Low Extra Delay Background Transport (LEDBAT), по которому ходит Bittorrent, Apple и наверняка что-то ещё. Теперь он ещё и инвестор, и очень похоже на то, что он планирует всерьёз развивать свой протокол и сделать его общепринятым (или хотя бы общеизвестным, как это произошло с LEDBAT).

Что ещё смущает


Помимо полной зависимости от одного разработчика, есть и другие странности вокруг этого проекта.
  • О нём никто нигде не пишет. Ни на HN, ни в бложиках или твиттерах. Полный информационный вакуум. Я даже не знаю откуда про него узнал тот человек, который написал характеристику из начала поста.
  • Если идея действительно хороша, её, пользуясь личным брендом и авторитетом Шалунова, можно было бы давно раскрутить и обзвестись поддержкой крупных игроков (или крупного комьюнити). Ничего этого нет.
  • Clostra очень мутная студия. Прямо очень. У них крайне стрёмного вида сайт, на котором они представляют свой единственный продукт Keymaker (ну и NewNode), всё без примеров, отзывов, скриншотов и прочей туфты, обязательной для лендинга. Там просто воодушевляющий текст в размытых формулировках и иконки с ближайшего стока. Нельзя изучить команду, вакансии или вообще что-то узнать про эту контору. У них есть твиттер, который судя по всему ведёт бот, и заброшенный в момент создания фейсбук. Но при всей этой внешней стрёмности они в нескольких местах подчёркивают факт своего сотрудничества с госслужбами, особенно с Department of Defence. Есть три отзыва про устройство к ним на работу, где два резко негативные (например, Don't waste your time with Clostra. Something stinks about this scam, а один очень положительный. В общем, на первый взгляд от скама такой проект не отличить.


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



На правах рекламы


Эпичные серверы это надёжные VDS на базе KVM с новейшими процессорами AMD EPYC. Как и для других типов серверов, огромный выбор операционных систем для автоматической установки, есть возможность установить любую ОС с собственного ISO, удобная панель управления собственной разработки и посуточная оплата.

Подробнее..

Какой хороший Xiaomi GetApps. Не даёт опубликовать обновление приложения и сам его просит

06.07.2020 16:22:04 | Автор: admin

Статья и голосование о том можно ли пользоваться Xiaomi GetApps совместно или (упаси Боже) вместо Google Play, если ты разработчик.


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

Сказка
И вот, недавно, решил я обновить своё приложение в Xiaomi GetApps.
Загрузил новую версию (нужно было ещё загружать каждый раз иконку приложения и описание не только нововедений, но и описание самого приложения).
Смотрю, а регион распространения только Mainland China.

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

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

Мол, юзеры просят новую версию, что ты её не выкладываешь?
Я пишу по обратному адресу: overseas-dev@xiaomi.com
Что мол, я уже выложил новую версию, почему Вы не рассматриваете моё приложение?
Почти сразу приходит отказ в публикации новой версии приложения и текущую версию приложения тоже снимают с публикации
Причина в том, что приложение не переведено на Китайский язык.

Я говорю: Так я не хочу в Китае распростаранять своё приложение, хочу в США, Европе и России.
Они почти сразу: Ок. выйдите и зайдите в консоль разработчика снова.

Я захожу в консоль разработчика и вижу следующее:

Ну Россия есть, уже спасибо. А Украина? А СНГ? А Европа? Представлена только Испанией?

Ладно думаю, пусть будет только Россия, хочу опубликовать новую версию (текущая версии приложения так и не опубликована).
Хочу опубликовать, а кнопка публикации заблокирована.


Ну я снял disabled=disabled


Тогда хоть начали показываться ошибки. Сначала GetApps сказал мне, что, раз приложение только на Русском, то его можно публиковать только в России. А в/на Украние, Белоруссии и Германии рускоязычных очевидно нет? Хотя европейских стран вообще нет в моей GetApps console, кроме Испании.
Были ещё какие-то 1-2 ошибки, я поправил.
Ну думаю опубликую. Нажимаю и вижу:

Вижу ошибку Null. И что мне делать? написал на overseas-dev@xiaomi.com
Ноль реакции.

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

Хотел позвонить в Российское представительсво компании Xiaomi по телефону горячей линии
+7-800-775-66-15, мне отвечает автоинформатор, что абонент не доступен.

Звук ответа автоинформатора: gofile.io/d/zf0DQI

Это было 06.07.2020 в 11:59
в 14:45 телефон горячей линии заработал. Можно написать об этом обо всём на service.global@xiaomi.com, пока жду ответа от overseas-dev@xiaomi.com
upd 06.07.20 15:56: написал на service.global@xiaomi.com, поставил в копию overseas-dev@xiaomi.com и дал ссылку на эту статью.

Кстати, на описание приложения в GetApps дают всего 400 символов.

На описание приложения дают 400 символов, Карл.

А на описание нововедений дают 500 символов:



GetApps console сейчас показывает, что приложение не опубликовано в GetApps:
NOTHING HERE

Это я ещё опустил, как мне отвечали на китайском.

Xiaomi GetApps это сказка.
Подробнее..

Категории

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

© 2006-2020, personeltest.ru