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

Dodo is

Путь разработчика в SRE зачем идти в инфраструктуру и что из этого выйдет

30.06.2020 20:09:15 | Автор: admin
Около года назад я переквалифицировался из .NET-разработчика в SRE. В этой статье делюсь историей о том, как группа опытных разработчиков отложила в сторону C# и пошла изучать Linux, Terraform, Packer, рисовать NALSD и строить IaC, как мы применяли практики экстремального программирования для управления инфраструктурой компании, и что из этого вышло.




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

Сейчас стабильность и надёжность информационной системы в компании поддерживает команда SRE (Site Reliability Engineering), но так было не всегда.

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


Много лет я развивался как типичный fullstack-разработчик (и немного scrum-мастер), учился писать хороший код, применял практики из Extreme Programming и старательно уменьшал количество WTF в проектах, к которым прикасался. Но чем больше появлялось опыта в разработке ПО, тем больше я осознавал важность надёжных систем мониторинга и трейсинга приложений, качественных логов, тотального автоматического тестирования и механизмов, обеспечивающих высокую надёжность сервисов. И всё чаще стал заглядывать через забор к команде инфраструктуры.

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

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

Бермудский треугольник проблем


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

Проблемы разработчиков


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

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



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

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

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

Сейчас это не сложно. В последние годы появилось огромное количество инструментов, которые позволяют программистам заглянуть в мир эксплуатации и ничего не сломать: Prometheus, Zipkin, Jaeger, ELK стек, Kusto.

Тем не менее у многих разработчиков до сих пор есть серьёзные проблемы с теми, кого называют инфраструктурой/DevOpsами/SRE. В итоге программисты:

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

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

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

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

Проблемы инфраструктуры


Сложности есть и на другой стороне.

Сложно управлять десятками сервисов и окружений без качественного кода. У нас в GitHub сейчас больше 450 репозиториев. Часть из них не требует операционной поддержки, часть мертва и сохраняется для истории, но значительная часть содержит сервисы, которые нужно поддерживать. Им нужно где-то хоститься, нужен мониторинг, сбор логов, единообразные CI/CD-пайплайны.

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

  • 60 ролей;
  • 102 плейбука;
  • обвязка на Python и Bash;
  • тесты в Vagrant, запускаемые вручную.

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

Причина крылась в том, что этот код не использовал многие стандартные практики в мире разработки ПО. В нём не было CI/CD-пайплайна, а тесты были сложными и медленными, поэтому всем было лень или некогда запускать их вручную, а уж тем более писать новые. Такой код обречён, если над ним работает более одного человека.

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

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

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

Проблемы бизнеса


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

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

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

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

И что с этим делать?


Как решить все эти проблемы? Решение мы нашли в книге Site Reliability Engineering от Google. Когда прочли, поняли это то, что нам нужно.

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

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

Из хороших практик SRE у нас уже были:

  • механизмы мониторинга приложений и инфраструктуры (спойлер: это мы в 2018 думали, что они хорошие, а сейчас уже всё переписали);
  • процессы для дежурств 24/7 on-call;
  • практика ведения постмортемов по инцидентам и их анализ;
  • нагрузочное тестирование;
  • CI/CD-пайплайны для прикладного софта;
  • хорошие программисты, которые пишут хороший код;
  • евангелист SRE в команде инфраструктуры.

Но были и проблемы, которые хотелось решить в первую очередь:

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

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

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

Онбординг SRE-команды


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

На проект мы выделили 4 месяца и поставили три цели:

  1. Обучить программистов тем знаниям и навыкам, которые необходимы для дежурств и операционной деятельности в команде инфраструктуры.
  2. Написать IaC описание всей инфраструктуры в коде. Причём это должен быть полноценный программный продукт с CI/CD, тестами.
  3. Пересоздать всю нашу инфраструктуру из этого кода и забыть про ручное накликивание виртуалок мышкой в Azure.

Состав участников: 9 человек, 6 из них из команды разработки, 3 из инфраструктуры. На 4 месяца они должны были уйти из обычной работы и погрузиться в обозначенные задачи. Чтобы поддерживать жизнь в бизнесе, ещё 3 человека из инфраструктуры остались дежурить, заниматься операционкой и прикрывать тылы. В итоге проект заметно растянулся и занял больше пяти месяцев (с мая по октябрь 2019-го года).

Две составляющие онбординга: обучение и практика


Онбординг состоял из двух частей: обучения и работы над инфраструктурой в коде.

Обучение. На обучение выделялось минимум 3 часа в день:

  • на чтение статей и книг из списка литературы: Linux, сети, SRE;
  • на лекции по конкретным инструментам и технологиям;
  • на клубы по технологиям, например, по Linux, где мы разбирали сложные случаи и кейсы.

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

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



Практика. Вторая часть онбординга создание/описание инфраструктуры в коде. Эту часть разделили на несколько этапов.



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

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

Написание кода. Сюда входило само написание кода, создание CI/CD-пайплайнов, тестов и построение процессов вокруг всего этого. Мы написали код, который описывал и умел создавать с нуля нашу дев-инфраструктуру.

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

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

Наши инструменты для IaC
  • Terraform для описания текущей инфраструктуры.
  • Packer и Ansible для создания образов виртуальных машин.
  • Jsonnet и Python как основные языки разработки.
  • Облако Azure, потому что у нас там хостинг.
  • VS Code IDE, для которой создали единые настройки, расширенный набор плагинов, линтеров и прочего, чтобы писать унифицированный код и расшарили их между всеми разработчиками.
  • Практики разработки одна из основных вещей, ради которой затевался весь этот карнавал.


Практики Extreme Programming в инфраструктуре


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

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

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

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

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


В рамках проекта было два вида проблем:

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

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

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

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

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

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

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

Итоги онбординга


По итогам проекта онбординга (он завершился в октябре 2019 года) мы:

  • Создали полноценный программный продукт, который управляет нашей DEV-инфраструктурой, с собственным CI-пайплайном, с тестами и прочими атрибутами качественного программного продукта.
  • Удвоили количество людей, которые готовы дежурить и сняли нагрузку с текущей команды. Спустя ещё полгода эти люди стали полноценными SRE. Теперь они могут потушить пожар на проде, проконсультировать команду программистов по НФТ, или написать свою библиотеку для разработчиков.
  • Сместили майндсет в сторону идей SRE. Не только у участников проекта онбординга, но и у тех программистов из продуктовых команд, которые теперь могут разговаривать с нами на одном языке.
  • Сильно устали: и те, кто участвовал в онбординге, и те, кто участвовал в дежурствах.

Вместо выводов: инсайты, не наступайте на наши грабли


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

Инфраструктура пока в прошлом. Когда я учился на первом курсе (15 лет назад) и начинал изучать JavaScript, у меня из инструментов были NotePad ++ и Firebug для отладки. C этими инструментами уже тогда нужно было делать какие-то сложные и красивые вещи.

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

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

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

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

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

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

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

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

PPS: Эта статья написана по моему выступлению на DevOpsConf осенью 2019 года. С тех пор прошло довольно много времени, и теперь уже точно понятно, что всё было не зря: тойл теперь не съедает бОльшую часть времени инженеров, наша команда теперь может реализовывать крупные долгосрочные проекты по улучшению инфраструктуры в широком смысле, а программисты почти не жалуются на безумных DevOps-инженеров, которые только мешают жить.

PPPS: В этом году конференция, посвящённая DevOps-практикам, будет называться DevOps Live 2020. Изменения коснутся не только названия: в программе будет меньше докладов и больше интерактивных обсуждений, мастер-классов и воркшопов. Рецепты о том, как расти и перестраивать процессы с помощью DevOps-практик. Формат также изменится два блока по два дня и домашние задания между ними.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Первая мысль

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

Подробнее..

Анимация в Android плавные переходы фрагментов внутри Bottom Sheet

08.07.2020 18:05:40 | Автор: admin
Написано огромное количество документации и статей о важной визуальной составляющей приложений анимации. Несмотря на это мы смогли вляпаться в проблемы столкнулись с загвоздками при её реализации.

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



Бриллиантовый чекаут: предыстория


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


Сравнение старого и нового чекаута

Между собой мы называем новый экран шторка. На рисунке вы видите, в каком виде мы получили задание от дизайнеров. Данное дизайнерское решение является стандартным, известно оно под именем Bottom Sheet, описано в Material Design (в том числе для Android) и в разных вариациях используется во многих приложениях. Google предлагает нам два готовых варианта реализации: модальный (Modal) и постоянный (Persistent). Разница между этими подходами описана во многих и многих статьях.


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

Смотри, какая классная анимация на iOS. Давай так же сделаем?


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

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


Что имеем из коробки

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

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

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

В чём, собственно, проблема: где клиенту хорошо, там у нас ограничения


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

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

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

Предварительная разметка


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

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="wrap_content"    android:background="@drawable/dialog_gray200_background"    >   <androidx.fragment.app.FragmentContainerView      android:id="@+id/container"      android:layout_width="match_parent"      android:layout_height="match_parent"      /> </FrameLayout>

И файл dialog_gray200_background.xml выглядит так:

<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android">  <item>    <shape android:shape="rectangle">      <solid android:color="@color/gray200" />      <corners android:bottomLeftRadius="0dp" android:bottomRightRadius="0dp" android:topLeftRadius="10dp" android:topRightRadius="10dp" />    </shape>  </item></selector>

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

Первые попытки реализовать анимацию


animateLayoutChanges


Вспоминаем о древней эльфийской магии animateLayoutChanges, которая на самом деле представляет собой дефолтный LayoutTransition. Хотя animateLayoutChanges совершенно не рассчитан на смену фрагментов, есть надежда, что это поможет с анимацией высоты. Также FragmentContainerView не поддерживает animateLayoutChanges, поэтому меняем его на старый добрый FrameLayout.

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="wrap_content"    android:background="@drawable/dialog_gray200_background"    >   <FrameLayout      android:id="@+id/container"      android:layout_width="match_parent"      android:layout_height="match_parent"      android:animateLayoutChanges="true"      /> </FrameLayout>

Запускаем:

animateLayoutChanges

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

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

setCustomAnimations


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

to_right_out.xml

<?xml version="1.0" encoding="utf-8"?><set xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:duration="500"    android:interpolator="@android:anim/accelerate_interpolator">  <translate android:toXDelta="100%" /></set>

to_right_in.xml

<?xml version="1.0" encoding="utf-8"?><set xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:duration="500"    android:interpolator="@android:anim/accelerate_interpolator">  <translate android:fromXDelta="-100%" /></set>

to_left_out.xml

<?xml version="1.0" encoding="utf-8"?><set xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:duration="500"    android:interpolator="@android:anim/accelerate_interpolator">  <translate android:toXDelta="-100%" /></set>

to_left_in.xml

<?xml version="1.0" encoding="utf-8"?><set xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:duration="500"    android:interpolator="@android:anim/accelerate_interpolator">  <translate android:fromXDelta="100%" /></set>

И затем устанавливаем эти анимации в транзакцию:

fragmentManager    .beginTransaction()    .setCustomAnimations(R.anim.to_left_in, R.anim.to_left_out, R.anim.to_right_in, R.anim.to_right_out)    .replace(containerId, newFragment)    .addToBackStack(newFragment.tag)    .commit()

Получаем вот такой результат:


setCustomAnimation

Что мы имеем при такой реализации:

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

Это никуда не годится. Вывод: нужно что-то другое.

А может попробуем что-то внезапное: Shared Element Transition


Большинство Android-разработчиков знает про Shared Element Transition. Однако, хотя этот инструмент очень гибкий, многие сталкиваются с проблемами при его использовании и поэтому не очень любят применять его.


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

Чтобы задать Shared Element, нам нужно:

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

А что, если использовать корневую View фрагмента в качестве Shared Element? Возможно Shared Element Transition придумывали не для этого. Хотя если подумать, сложно найти аргумент, почему это решение не подойдёт. Мы хотим анимировать начальный элемент в конечный элемент между двумя фрагментами. Не вижу идеологического противоречия. Давайте попробуем сделать так!

Для каждого фрагмента, который находится внутри шторки, для корневой View указываем атрибут transitionName с одинаковым значением:

<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout     xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="wrap_content"    android:transitionName="checkoutTransition"    >

Важно: это будет работать, поскольку мы используем REPLACE в транзакции фрагментов. Если вы используете ADD (или используете ADD и скрываете предыдущий фрагмент с помощью previousFragment.hide() [не надо так делать]), то transitionName придётся задавать динамически и очищать после завершения анимации. Так приходится делать, потому что в один момент времени в текущей иерархии View не может быть две View с одинаковым transitionName. Осуществить это можно, но будет лучше, если вы сможете обойтись без такого хака. Если вам всё-таки очень нужно использовать ADD, вдохновение для реализации можно найти в этой статье.

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

newFragment.sharedElementEnterTransition = AutoTransition()

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

fragmentManager    .beginTransaction()    .apply{      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {        addSharedElement(currentFragment.requireView(), currentFragment.requireView().transitionName)        setReorderingAllowed(true)      }    }    .replace(containerId, newFragment)    .addToBackStack(newFragment.tag)    .commit()

Важно: обратите внимание, что transitionName (как и весь Transition API) доступен начиная с версии Android Lollipop.

Посмотрим, что получилось:


AutoTransition

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

Раз стандартная реализация нам не подошла, что нужно сделать? Конечно же, нужно переписать всё на Flutter написать свой Transition!

Пишем свой Transition


Transition это класс из Transition API, который отвечает за создание анимации между двумя сценами (Scene). Основные элементы этого API:

  • Scene это расположение элементов на экране в определённый момент времени (layout) и ViewGroup, в которой происходит анимация (sceneRoot).
  • Начальная сцена (Start Scene) это Scene в начальный момент времени.
  • Конечная сцена (End Scene) это Scene в конечный момент времени.
  • Transition класс, который собирает свойства начальной и конечной сцены и создаёт аниматор для анимации между ними.

В классе Transition мы будем использовать четыре метода:

  • fun getTransitionProperties(): Array. Данный метод должен вернуть набор свойств, которые будут анимироваться. Из этого метода нужно вернуть массив строк (ключей) в свободном виде, главное, чтобы методы captureStartValues и captureEndValues (описанные далее) записали свойства с этими ключами. Пример будет далее.
  • fun captureStartValues(transitionValues: TransitionValues). В данном методе мы получаем нужные свойства layout'а начальной сцены. Например, мы можем получить начальное расположение элементов, высоту, прозрачность и так далее.
  • fun captureEndValues(transitionValues: TransitionValues). Такой же метод, только для получения свойств layout'а конечной сцены.
  • fun createAnimator(sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues?): Animator?. Этот метод должен использовать свойства начальной и конечной сцены, собранные ранее, чтобы создать анимацию между этими свойствами. Обратите внимание, что если свойства между начальной и конечной сценой не поменялись, то данный метод не вызовется вовсе.

Реализуем свой Transition за девять шагов


  1. Создаём класс, который представляет Transition.

    @TargetApi(VERSION_CODES.LOLLIPOP)class BottomSheetSharedTransition : Transition {@Suppress("unused")constructor() : super() @Suppress("unused")constructor(      context: Context?,       attrs: AttributeSet?) : super(context, attrs)}
    
    Напоминаю, что Transition API доступен с версии Android Lollipop.
  2. Реализуем getTransitionProperties.

    Поскольку мы хотим анимировать высоту View, заведём константу PROP_HEIGHT, соответствующую этому свойству (значение может быть любым) и вернём массив с этой константой:

    companion object {  private const val PROP_HEIGHT = "heightTransition:height"   private val TransitionProperties = arrayOf(PROP_HEIGHT)} override fun getTransitionProperties(): Array<String> = TransitionProperties
    
  3. Реализуем captureStartValues.

    Нам нужно запомнить высоту той View, которая хранится в параметре transitionValues. Значение высоты нам нужно записать в поле transitionValues.values (он имеет тип Map) c ключом PROP_HEIGHT:

    override fun captureStartValues(transitionValues: TransitionValues) {  transitionValues.values[PROP_HEIGHT] = transitionValues.view.height}
    

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

    override fun captureStartValues(transitionValues: TransitionValues) {  // Запоминаем начальную высоту View...  transitionValues.values[PROP_HEIGHT] = transitionValues.view.height   // ... и затем закрепляем высоту контейнера фрагмента  transitionValues.view.parent    .let { it as? View }    ?.also { view ->        view.updateLayoutParams<ViewGroup.LayoutParams> {            height = view.height        }    } }
    
  4. Реализуем captureEndValues.

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

    override fun captureEndValues(transitionValues: TransitionValues) {  // Измеряем и запоминаем высоту View  transitionValues.values[PROP_HEIGHT] = getViewHeight(transitionValues.view.parent as View)}
    

    И метод getViewHeight:

    private fun getViewHeight(view: View): Int {  // Получаем ширину экрана  val deviceWidth = getScreenWidth(view)   // Попросим View измерить себя при указанной ширине экрана  val widthMeasureSpec = MeasureSpec.makeMeasureSpec(deviceWidth, MeasureSpec.EXACTLY)  val heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)   return view      // измеряем      .apply { measure(widthMeasureSpec, heightMeasureSpec) }      // получаем измеренную высоту      .measuredHeight      // если View хочет занять высоту больше доступной высоты экрана, мы должны вернуть высоту экрана      .coerceAtMost(getScreenHeight(view))} private fun getScreenHeight(view: View) =  getDisplaySize(view).y - getStatusBarHeight(view.context) private fun getScreenWidth(view: View) =  getDisplaySize(view).x private fun getDisplaySize(view: View) =  Point().also {    (view.context.getSystemService(        Context.WINDOW_SERVICE    ) as WindowManager).defaultDisplay.getSize(it)  } private fun getStatusBarHeight(context: Context): Int =  context.resources      .getIdentifier("status_bar_height", "dimen", "android")      .takeIf { resourceId -> resourceId > 0 }      ?.let { resourceId -> context.resources.getDimensionPixelSize(resourceId) }      ?: 0
    

    Таким образом, мы знаем начальную и конечную высоту контейнера, и теперь дело за малым создать анимацию.
  5. Реализация анимации. Fade in.

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

    private fun prepareFadeInAnimator(view: View): Animator =   ObjectAnimator.ofFloat(view, "alpha", 0f, 1f) 
    
  6. Реализация анимации. Анимация высоты.

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

    private fun prepareHeightAnimator(    startHeight: Int,    endHeight: Int,    view: View) = ValueAnimator.ofInt(startHeight, endHeight)    .apply {        val container = view.parent.let { it as View }                // изменяем высоту контейнера фрагментов        addUpdateListener { animation ->            container.updateLayoutParams<ViewGroup.LayoutParams> {                height = animation.animatedValue as Int            }        }    }
    

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

    private fun prepareHeightAnimator(    startHeight: Int,    endHeight: Int,    view: View) = ValueAnimator.ofInt(startHeight, endHeight)    .apply {        val container = view.parent.let { it as View }                // изменяем высоту контейнера фрагментов        addUpdateListener { animation ->            container.updateLayoutParams<ViewGroup.LayoutParams> {                height = animation.animatedValue as Int            }        }                // окончании анимации устанавливаем высоту контейнера WRAP_CONTENT         doOnEnd {            container.updateLayoutParams<ViewGroup.LayoutParams> {                height = ViewGroup.LayoutParams.WRAP_CONTENT            }        }    }
    

    Теперь всего лишь нужно использовать аниматоры, созданные этими функциями.
  7. Реализация анимации. createAnimator.

    override fun createAnimator(    sceneRoot: ViewGroup?,    startValues: TransitionValues?,    endValues: TransitionValues?): Animator? {    if (startValues == null || endValues == null) {        return null    }     val animators = listOf<Animator>(        prepareHeightAnimator(            startValues.values[PROP_HEIGHT] as Int,            endValues.values[PROP_HEIGHT] as Int,            endValues.view        ),        prepareFadeInAnimator(endValues.view)    )     return AnimatorSet()        .apply {            interpolator = FastOutSlowInInterpolator()            duration = ANIMATION_DURATION            playTogether(animators)        }}
    
  8. Всегда анимируем переход.

    Последний нюанс касательно реализации данного Transititon'а. Звёзды могут сойтись таким образом, что высота начального фрагмента будет точно равна высоте конечного фрагмента. Такое вполне может быть, если оба фрагмента занимают всю высоту экрана. В таком случае метод createAnimator не будет вызван совсем. Что же произойдёт?

    • Не будет Fade'а нового фрагмента, он просто резко появится на экране.
    • Поскольку в методе captureStartValues мы зафиксировали высоту контейнера, а анимации не произойдёт, высота контейнера никогда не станет равной WRAP_CONTENT.

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

    companion object {    private const val PROP_HEIGHT = "heightTransition:height"    private const val PROP_VIEW_TYPE = "heightTransition:viewType"     private val TransitionProperties = arrayOf(PROP_HEIGHT, PROP_VIEW_TYPE)} override fun getTransitionProperties(): Array<String> = TransitionProperties override fun captureStartValues(transitionValues: TransitionValues) {    // Запоминаем начальную высоту View...    transitionValues.values[PROP_HEIGHT] = transitionValues.view.height    transitionValues.values[PROP_VIEW_TYPE] = "start"     // ... и затем закрепляем высоту контейнера фрагмента    transitionValues.view.parent        .let { it as? View }        ?.also { view ->            view.updateLayoutParams<ViewGroup.LayoutParams> {                height = view.height            }        } } override fun captureEndValues(transitionValues: TransitionValues) {    // Измеряем и запоминаем высоту View    transitionValues.values[PROP_HEIGHT] = getViewHeight(transitionValues.view.parent as View)    transitionValues.values[PROP_VIEW_TYPE] = "end"}
    

    Обратите внимание, добавилось свойство PROP_VIEW_TYPE, и в методах captureStartValues и captureEndValues записываем разные значения этого свойства. Всё, транзишн готов!
  9. Применяем Transition.

    newFragment.sharedElementEnterTransition = BottomSheetSharedTransition()
    

Асинхронная загрузка данных


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

Всё вместе: что в итоге получилось


С новым BottomSheetSharedTransition и использованием postponeEnterTransition при асинхронной загрузке данных у нас получилась такая анимация:

Готовый transition

Под спойлером готовый класс BottomSheetSharedTransition
package com.maleev.bottomsheetanimation import android.animation.Animatorimport android.animation.AnimatorSetimport android.animation.ObjectAnimatorimport android.animation.ValueAnimatorimport android.annotation.TargetApiimport android.content.Contextimport android.graphics.Pointimport android.os.Buildimport android.transition.Transitionimport android.transition.TransitionValuesimport android.util.AttributeSetimport android.view.Viewimport android.view.ViewGroupimport android.view.WindowManagerimport android.view.animation.AccelerateInterpolatorimport androidx.core.animation.doOnEndimport androidx.core.view.updateLayoutParams @TargetApi(Build.VERSION_CODES.LOLLIPOP)class BottomSheetSharedTransition : Transition {     @Suppress("unused")    constructor() : super()     @Suppress("unused")    constructor(        context: Context?,        attrs: AttributeSet?    ) : super(context, attrs)     companion object {        private const val PROP_HEIGHT = "heightTransition:height"         // the property PROP_VIEW_TYPE is workaround that allows to run transition always        // even if height was not changed. It's required as we should set container height        // to WRAP_CONTENT after animation complete        private const val PROP_VIEW_TYPE = "heightTransition:viewType"        private const val ANIMATION_DURATION = 400L         private val TransitionProperties = arrayOf(PROP_HEIGHT, PROP_VIEW_TYPE)    }     override fun getTransitionProperties(): Array<String> = TransitionProperties     override fun captureStartValues(transitionValues: TransitionValues) {        // Запоминаем начальную высоту View...        transitionValues.values[PROP_HEIGHT] = transitionValues.view.height        transitionValues.values[PROP_VIEW_TYPE] = "start"         // ... и затем закрепляем высоту контейнера фрагмента        transitionValues.view.parent            .let { it as? View }            ?.also { view ->                view.updateLayoutParams<ViewGroup.LayoutParams> {                    height = view.height                }            }     }     override fun captureEndValues(transitionValues: TransitionValues) {        // Измеряем и запоминаем высоту View        transitionValues.values[PROP_HEIGHT] = getViewHeight(transitionValues.view.parent as View)        transitionValues.values[PROP_VIEW_TYPE] = "end"    }     override fun createAnimator(        sceneRoot: ViewGroup?,        startValues: TransitionValues?,        endValues: TransitionValues?    ): Animator? {        if (startValues == null || endValues == null) {            return null        }         val animators = listOf<Animator>(            prepareHeightAnimator(                startValues.values[PROP_HEIGHT] as Int,                endValues.values[PROP_HEIGHT] as Int,                endValues.view            ),            prepareFadeInAnimator(endValues.view)        )         return AnimatorSet()            .apply {                duration = ANIMATION_DURATION                playTogether(animators)            }    }     private fun prepareFadeInAnimator(view: View): Animator =        ObjectAnimator            .ofFloat(view, "alpha", 0f, 1f)            .apply { interpolator = AccelerateInterpolator() }     private fun prepareHeightAnimator(        startHeight: Int,        endHeight: Int,        view: View    ) = ValueAnimator.ofInt(startHeight, endHeight)        .apply {            val container = view.parent.let { it as View }             // изменяем высоту контейнера фрагментов            addUpdateListener { animation ->                container.updateLayoutParams<ViewGroup.LayoutParams> {                    height = animation.animatedValue as Int                }            }             // окончании анимации устанавливаем высоту контейнера WRAP_CONTENT            doOnEnd {                container.updateLayoutParams<ViewGroup.LayoutParams> {                    height = ViewGroup.LayoutParams.WRAP_CONTENT                }            }        }     private fun getViewHeight(view: View): Int {        // Получаем ширину экрана        val deviceWidth = getScreenWidth(view)         // Попросим View измерить себя при указанной ширине экрана        val widthMeasureSpec =            View.MeasureSpec.makeMeasureSpec(deviceWidth, View.MeasureSpec.EXACTLY)        val heightMeasureSpec =            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)         return view            // измеряем:            .apply { measure(widthMeasureSpec, heightMeasureSpec) }            // получаем измеренную высоту:            .measuredHeight            // если View хочет занять высоту больше доступной высоты экрана, мы должны вернуть высоту экрана:            .coerceAtMost(getScreenHeight(view))    }     private fun getScreenHeight(view: View) =        getDisplaySize(view).y - getStatusBarHeight(view.context)     private fun getScreenWidth(view: View) =        getDisplaySize(view).x     private fun getDisplaySize(view: View) =        Point().also { point ->            view.context.getSystemService(Context.WINDOW_SERVICE)                .let { it as WindowManager }                .defaultDisplay                .getSize(point)        }     private fun getStatusBarHeight(context: Context): Int =        context.resources            .getIdentifier("status_bar_height", "dimen", "android")            .takeIf { resourceId -> resourceId > 0 }            ?.let { resourceId -> context.resources.getDimensionPixelSize(resourceId) }            ?: 0}


Когда у нас есть готовый класс Transition'а, его применение сводится к простым шагам:

Шаг 1. При транзакции фрагмента добавляем Shared Element и устанавливаем Transition:

private fun transitToFragment(newFragment: Fragment) {    val currentFragmentRoot = childFragmentManager.fragments[0].requireView()     childFragmentManager        .beginTransaction()        .apply {            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {                addSharedElement(currentFragmentRoot, currentFragmentRoot.transitionName)                setReorderingAllowed(true)                 newFragment.sharedElementEnterTransition = BottomSheetSharedTransition()            }        }        .replace(R.id.container, newFragment)        .addToBackStack(newFragment.javaClass.name)        .commit()}

Шаг 2. В разметке фрагментов (текущего фрагмента и следующего), которые должны анимироваться внутри BottomSheetDialogFragment, устанавливаем transitionName:

<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout     xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="wrap_content"    android:transitionName="checkoutTransition"    >

На этом всё, конец.

А можно было сделать всё иначе?


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

  • Отказаться от фрагментов, использовать один фрагмент с множеством View и анимировать конкретные View. Так вы получите больший контроль над анимацией, но потеряете преимущества фрагментов: нативную поддержку навигации и готовую обработку жизненного цикла (придётся реализовывать это самостоятельно).
  • Использовать MotionLayout. Технология MotionLayout на данный момент всё ещё находится на стадии бета, но выглядит очень многообещающе, и уже есть официальные примеры, демонстрирующие красивые переходы между фрагментами.
  • Не использовать анимацию. Да, наш дизайн является частным случаем, и вы вполне можете счесть анимацию в данном случае избыточной. Вместо этого можно показывать один Bottom Sheet поверх другого или скрывать один Bottom Sheet и следом показывать другой.
  • Отказаться от Bottom Sheet совсем. Нет изменения высоты контейнера фрагментов нет проблем.
Демо проект можно найти вот тут на GitHub. А вакансию Android-разработчика (Нижний Новгород) вот здесь на Хабр Карьера.
Подробнее..

Сказ о том, как каскадное удаление в Realm долгий запуск победило

30.07.2020 18:15:54 | Автор: admin
Все пользователи считают быстрый запуск и отзывчивый UI в мобильных приложениях само собой разумеющимся. Если приложение запускается долго, пользователь начинает грустить и злиться. Запросто можно подпортить клиентский опыт или вовсе потерять пользователя ещё до того, как он начал пользоваться приложением.

Однажды мы обнаружили, что приложение Додо Пицца запускается в среднем 3 секунды, а у некоторых счастливчиков 15-20 секунд.

Под катом история с хеппи эндом: про рост базы данных Realm, утечку памяти, то, как мы копили вложенные объекты, а после взяли себя в руки и всё починили.





Автор статьи: Максим Качинкин Android-разработчик в Додо Пицце.



Три секунды от клика на иконку приложения до onResume() первого активити бесконечность. А у некоторых пользователей время запуска доходило до 15-20 секунд. Как такое вообще возможно?

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

Поиск и анализ проблемы


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

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

Долго это сколько? Согласно Google-документации, если холодный старт приложения занимает менее 5 секунд, то это считается как бы нормально. Android-приложение Додо Пиццы запускалось (согласно Firebase метрике _app_start) при холодном старте в среднем за 3 секунды Not great, not terrible, как говорится.

Но потом стали появляться жалобы, что приложение запускается очень-очень-очень долго! Для начала мы решили измерить, что же такое очень-очень-очень долго. И воспользовались для этого Firebase trace App start trace.



Этот стандартный трейс измеряет время между моментом, когда пользователь открывает приложение, и моментом, когда выполнится onResume() первого активити. В Firebase Console эта метрика называется _app_start. Выяснилось что:

  • Время запуска у пользователей выше 95-го процентиля составляет почти 20 секунд (у некоторых и больше), несмотря на то, что медианное время холодного запуска менее 5 секунд.
  • Время запуска величина не постоянная, а растущая со временем. Но иногда наблюдаются падения. Эту закономерность мы нашли, когда увеличили масштаб анализа до 90 дней.



На ум пришло две мысли:

  1. Что-то утекает.
  2. Это что-то после релиза сбрасывается и потом утекает вновь.

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

Что не так с базой данных Realm


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

adb exec-out run-as ${PACKAGE_NAME} cat files/${DB_NAME}

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


На картинке показан фрагмент Realm Studio для двух файлов: слева база приложения спустя некоторое время после установки, справа после активного использования. Видно, что количество объектов ImageEntity и MoneyType сильно выросло (на скриншоте показано количество объектов каждого типа).

Связь роста базы данных с временем запуска


Неконтролируемый рост базы данных это очень плохо. Но как это влияет на время запуска приложения? Померить это достаточно просто через ActivityManager. Начиная с Android 4.4, logcat отображает лог со строкой Displayed и временем. Это время равно промежутку с момента запуска приложения до конца отрисовки активити. За это время происходят события:

  • Запуск процесса.
  • Инициализация объектов.
  • Создание и инициализация активити.
  • Создание лейаута.
  • Отрисовка приложения.

Нам подходит. Если запустить ADB с флагами -S и -W, то можно получить расширенный вывод с временем запуска:

adb shell am start -S -W ru.dodopizza.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN

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



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

Причины бесконечного роста базы данных


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

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

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

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

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

Утечка данных без каскадного удаления


Как именно утекают данные, если надеяться на несуществующее каскадное удаление? Если у вас есть вложенные Realm-объекты, то их нужно обязательно удалять.
Рассмотрим (почти) реальный пример. У нас есть объект CartItemEntity:

@RealmClassclass CartItemEntity( @PrimaryKey override var id: String? = null, ... var name: String = "", var description: String = "", var image: ImageEntity? = null, var category: String = MENU_CATEGORY_UNKNOWN_ID, var customizationEntity: CustomizationEntity? = null, var cartComboProducts: RealmList<CartProductEntity> = RealmList(), ...) : RealmObject()

У продукта в корзине есть разные поля, в том числе картинка ImageEntity, настроенные ингредиенты CustomizationEntity. Также продуктом в корзине может являтся комбо со своим набором продуктов RealmList (CartProductEntity). Все перечисленные поля являются Realm-объектами. Если мы вставим новый объект (copyToRealm() / copyToRealmOrUpdate()) с таким же id, то этот объект полностью перезапишется. Но все внутренние объекты (image, customizationEntity и cartComboProducts) потеряют связь с родительским и останутся в базе.

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

Когда мы работаем с Realm, то должны явно проходить по всем элементам и явно все удалять перед такими операциями. Это можно сделать, например, вот так:

val entity = realm.where(CartItemEntity::class.java).equalTo("id", id).findFirst()if (first != null) { deleteFromRealm(first.image) deleteFromRealm(first.customizationEntity) for(cartProductEntity in first.cartComboProducts) {   deleteFromRealm(cartProductEntity) } first.deleteFromRealm()}// и потом уже сохраняем

Если сделать так, то всё будет работать как надо. В данном примере мы предполагаем, что внутри image, customizationEntity и cartComboProducts нет других вложенных Realm-объектов, поэтому нет других вложенных циклов и удалений.

Решение по-быстрому


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

interface NestedEntityAware { fun getNestedEntities(): Collection<RealmObject?>}

И реализовали его в наших Realm-объектах:

@RealmClassclass DataPizzeriaEntity( @PrimaryKey var id: String? = null, var name: String? = null, var coordinates: CoordinatesEntity? = null, var deliverySchedule: ScheduleEntity? = null, var restaurantSchedule: ScheduleEntity? = null, ...) : RealmObject(), NestedEntityAware { override fun getNestedEntities(): Collection<RealmObject?> {   return listOf(       coordinates,       deliverySchedule,       restaurantSchedule   ) }}

В getNestedEntities мы возвращаем всех детей плоским списком. А каждый дочерний объект также может реализовывать интерфейс NestedEntityAware, сообщая что у него есть внутренние Realm-объекты на удаление, например ScheduleEntity:

@RealmClassclass ScheduleEntity( var monday: DayOfWeekEntity? = null, var tuesday: DayOfWeekEntity? = null, var wednesday: DayOfWeekEntity? = null, var thursday: DayOfWeekEntity? = null, var friday: DayOfWeekEntity? = null, var saturday: DayOfWeekEntity? = null, var sunday: DayOfWeekEntity? = null) : RealmObject(), NestedEntityAware { override fun getNestedEntities(): Collection<RealmObject?> {   return listOf(       monday, tuesday, wednesday, thursday, friday, saturday, sunday   ) }}

И так далее вложенность объектов может повторяться.

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

fun <T> Realm.deleteAllNestedEntities(entities: Collection<T>, entityClass: Class<out RealmObject>, idMapper: (T) -> String, idFieldName : String = "id" ) { val existedObjects = where(entityClass)     .`in`(idFieldName, entities.map(idMapper).toTypedArray())     .findAll() deleteNestedRecursively(existedObjects)}private fun Realm.deleteNestedRecursively(entities: Collection<RealmObject?>) { for(entity in entities) {   entity?.let { realmObject ->     if (realmObject is NestedEntityAware) {       deleteNestedRecursively((realmObject as NestedEntityAware).getNestedEntities())     }     realmObject.deleteFromRealm()   } }}

Мы проделали это с самыми быстрорастущими объектами и проверили, что получилось.



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

Решение по-нормальному


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

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

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

RealmModel::class.java.isAssignableFrom(field.type)RealmList::class.java.isAssignableFrom(field.type)

Если поле является RealmModel или RealmList, то сложим объект этого поля в список вложенных объектов. Всё точно так же, как мы делали выше, только тут оно будет делаться само. Сам метод каскадного удаления получается очень простым и выглядит так:

fun <T : Any> Realm.cascadeDelete(entities: Collection<T?>) { if(entities.isEmpty()) {   return } entities.filterNotNull().let { notNullEntities ->   notNullEntities       .filterRealmObject()       .flatMap { realmObject -> getNestedRealmObjects(realmObject) }       .also { realmObjects -> cascadeDelete(realmObjects) }   notNullEntities       .forEach { entity ->         if((entity is RealmObject) && entity.isValid) {           entity.deleteFromRealm()         }       } }}

Экстеншн filterRealmObject отфильтровывает и пропускает только Realm-объекты. Метод getNestedRealmObjects через рефлексию находит все вложенные Realm-объекты и складывает их в линейный список. Далее рекурсивно делаем всё то же самое. При удалении нужно проверить объект на валидность isValid, потому что может быть такое, что разные родительские объекты могут иметь вложенные одинаковые. Этого лучше не допускать и просто использовать автогенерацию id при создании новых объектов.


Полная реализация метода getNestedRealmObjects
private fun getNestedRealmObjects(realmObject: RealmObject) : List<RealmObject> { val nestedObjects = mutableListOf<RealmObject>() val fields = realmObject.javaClass.superclass.declaredFields// Проверяем каждое поле, не является ли оно RealmModel или списком RealmList fields.forEach { field ->   when {     RealmModel::class.java.isAssignableFrom(field.type) -> {       try {         val child = getChildObjectByField(realmObject, field)         child?.let {           if (isInstanceOfRealmObject(it)) {             nestedObjects.add(child as RealmObject)           }         }       } catch (e: Exception) { ... }     }     RealmList::class.java.isAssignableFrom(field.type) -> {       try {         val childList = getChildObjectByField(realmObject, field)         childList?.let { list ->           (list as RealmList<*>).forEach {             if (isInstanceOfRealmObject(it)) {               nestedObjects.add(it as RealmObject)             }           }         }       } catch (e: Exception) { ... }     }   } } return nestedObjects}private fun getChildObjectByField(realmObject: RealmObject, field: Field): Any? { val methodName = "get${field.name.capitalize()}" val method = realmObject.javaClass.getMethod(methodName) return method.invoke(realmObject)}


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

override fun <T : Entity> insert( entityInformation: EntityInformation, entities: Collection<T>): Collection<T> = entities.apply { realmInstance.cascadeDelete(getManagedEntities(entityInformation, this)) realmInstance.copyFromRealm(     realmInstance         .copyToRealmOrUpdate(this.map { entity -> entity as RealmModel } ))}

Сначала метод getManagedEntities получает все добавляемые объекты, а потом метод cascadeDelete рекурсивно удаляет все собранные объекты перед записью новых. В итоге мы используем этот подход по всему приложению. Утечки памяти в Realm полностью исчезли. Проведя тот же замер зависимости времени запуска от количества холодных запусков приложения, мы видим результат.



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

Результаты и выводы


Постоянно растущая база данных Realm сильно замедляла запуск приложения. Мы выпустили обновление с собственным каскадным удалением вложенных объектов. И теперь отслеживаем и оцениваем, как наше решение повлияло на время запуска приложения через метрику _app_start.



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



Если посмотреть на семидневный график, то метрика _app_start полностью выглядит адекватной и составляет меньше 1 секунды.

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

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

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

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

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

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

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



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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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


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

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

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

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


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

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


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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

Что в итоге


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

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

Планы


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

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

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

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

Напоследок


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

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

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

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

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

Категории

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

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru