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

Блог компании dodo pizza engineering

Ортолинейная сплит клавиатура это что такое? Обзор Iris Keyboard

06.08.2020 18:12:42 | Автор: admin
За сто с лишним лет клавиатуры сильно изменились внешне: стали лёгкими, тонкими, есть даже проекционные со сканером. Но рынок ничего не меняет в их раскладке (QWERTY появилась в 1890 году), клавиатуры не стали удобней для пальцев, не адаптированы для положения кистей и предплечий. Всё человечество стало проводить жизнь за компьютером, но эргономика главного инструмента ввода осталась в позапрошлом веке.

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




Недавно я купил механическую клавиатуру Vortex Core RGB. Мне понравилась механика, новое расположение курсоров и скобок, нормально привык к меньшему количеству клавиш. Но мне не хватало стабильности печати, поэтому я стал гонять тренажёр. Пришлось поставить руки правильно, но они стали уставать от неестественно прямого положения.

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


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

Что я ищу? Суммируя статью про дизайн клавиатур:

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

В поисках эргономики


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

  1. В офисе я уже видел Microsoft Sculpt, на ней руки лежат правильно, но кнопки всё ещё как на печатной машинке.

  2. Изгиба можно добиться иначе, совсем разделив клавиатуру пополам, например, Ultimate hacking keyboard.

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

  4. Можно разделить ортолинейную клавиатуру пополам, например, Levinson. Уже лучше, но есть ли ещё?



    Можно ли сместить кнопки вертикально, чтобы они легли ровно под пальцы? Конечно.
    Вариантов не очень много, но выбор есть.
  5. Model 01 keyboard крута, каждая клавиша уникальна, сделана под палец. Стоит $300, но снята с продажи, ждём новую.

  6. Ergodox EZ мне не понравилась: слишком большая даже на фото, не понимаю, что делать с таким количеством кнопок у большого пальца. С ценником тоже $300+.

  7. Можно сделать шаг назад, не разделять, а просто правильно поставить клавиши. Так сделано в Atreus, но продажи ещё не начались (по плану в августе-сентябре).



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

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

С софтом тоже просто: контроллер это Arduino, а для него есть опенсорсный QMK, который стал стандартом с кучей функций.

DIY: осталось только припаять


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

  1. Много кнопок у Helix.

  2. Средне у Iris.

  3. Мало кнопок у Kyria.

  4. Совсем мало у Jian, но зато адаптирован к русской раскладке.

  5. Пальцы двигаются не по прямой, а по дуге, можно ли так поставить клавиши? Да, если у вас Dactyl-ManuForm. Надо будет напечатать корпус на 3D-принтере, платы внутри нет совсем, всё на проводах. Сборка не самая простая.


Больше всего мне понравилась клавиатура Iris (под номером 2 в этом списке). Платы этого типа делают не первый день, к четвёртой версии плате уже не нужен отдельный контроллер, ATmega32U4 встроен и не занимает места.



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

Iris Keyboard: собираем сами


Сколько это стоит?


Купить все компоненты в одном месте не получится. Плата и корпус на сайте из Америки, кнопки на Гикбордс, колпачки на Алиэкспресс. Кнопки можно найти и на Авито, в Москве всегда есть из чего выбрать. Всё вместе вышло на 14 тысяч рублей (но изначально я забыл купить боковину, которая стоила ещё 2 тысячи).


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

Можно сделать дороже: взять всё самое крутое. Набор красивых клавиш запросто может стоить $150, крутые переключатели Zilents обойдутся ещё в $60. Это без учёта доставки, которая обычно стоит около $30 на позицию. В итоге можно собрать клавиатуру и за 30 тысяч рублей ( $450, дальше оценивайте по курсу).

Как это спаять?


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

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


Рабочий стол в процессе

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



Плата крутая.

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

Шесть светодиодов уже встроено в плату, но можно допаять ещё под каждую кнопку.

Подключение через USB-C, платы соединяются через TRRS (это разъём как у наушников).

Поддерживает несколько видов переключателей: Cherry MX (и даже PCB Mounted), низкопрофильные Kailh choc (но только первой версии), Alps.

Ну и просто цвет красивый.



Не дожидаясь пайки, я вставил все клавиши и пощёлкал. Кайф, всё под рукой.



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

Пошаговая сборка клавиатуры


  1. Вставляете угловые механизмы.

  2. Насаживаете на плату, выравниваете, припаиваете.

  3. Добавляете остальные кнопки, припаиваете.

  4. Прикручиваете низ корпуса, добавляете кейкапы. Готово!


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

На фото видны переключатели двух цветов, это разные типы кнопок:

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

Смысл в этом такой: у кнопок shift, control, alt не нужен бугорок, это вспомогательные клавиши, они нажимаются вместе с какой-то другой. Заранее я не знал, где они стоят, поэтому позже пришлось все ещё пару раз перепаять, оловоотсос пригодился.

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



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

Обзор Iris Keyboard: от эргономики до ништяков


Разделённая на две части


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

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



Правильное расположение клавиатуры убрало неприятные ощущения в суставах, которые были после обычных клавиатур.

С особым выравниванием кнопок


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



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

Высокая


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

Уменьшить сам корпус. Убрать дно и приклеить коврик от мышки к плате (!), либо просто уменьшить расстояние, поставив новые распорки. Сам корпус может быть не акриловым в 3мм, а металлическим, получится ещё тоньше. Без корпуса высота уменьшается на треть.



Заменить переключатели на низкопрофильные, купить к ним низкие кнопки. Khail chock поддерживаются, заказал, жду. С новыми клавишами и без корпуса высота уменьшится вдвое. Это уже почти как Apple Magic Keyboard.

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



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



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

Готовая к наклонам


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



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



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

С кастомизированными кнопками


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



Или быть проще совсем без надписей и дополнительных кнопок.



Самая важная характеристика кнопки это её профиль. Их много разных.


Проще всего на ортолинейные клавиатуру найти кейкапы плоского DSA-профиля.



DSA мне не очень понравился, слишком одинаковые клавиши. Кнопки с наклоном интереснее, например, OEM-профиль с Алиэкспресс.



Для ортолинейных клавиатур выбор раскладок ограничен, потому что нужен Shift, Control и Enter размером в одну кнопку, а много где их нет в комплекте. В некоторых наборах можно взять буквенные кнопки от основного набора, а остальные докупить отдельно. Цена поднимается существенно, обычно выше $100 только за клавиши. Например, мне понравился GMK Nautilus, но нужный набор будет стоить $190. Это столько же, сколько стоит клавиатура сейчас.



Размером 60% от привычной


У клавиатуры меньше клавиш, нет ряда F1-F12, но ряд цифр остался, а значит не нужно сразу ломать все привычки. Иногда удобней нажать отдельную клавишу, чем уходить на другой слой, где цифры под пальцами. Если бы их не было, то шорткат скриншота на Mac стал бы ещё сложнее: вместо сmd + shift + 3 надо нажать сmd + shift + Fn + e.

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

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

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

Кнопки под большим пальцем можно заменить, варианта три.

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

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

  3. А можно вместо верхней кнопки поставить крутилку. Об этом отдельно.


Крутилки


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

  • скролить страницу (имитировать колесико мыши);
  • двигать курсор;
  • менять громкость.

Я выбрал движение курсора, на второй слой добавил громкость. Самым весёлым оказалось крутить Youtube, ведь курсоры его проматывают.

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

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

Раскладка


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



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

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

Сначала кажется, что невозможно запомнить. На самом деле всё делится на этапы:

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

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

Подписи для кнопок


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

Скобки и русский язык


Для программирования я поставил скобки вдоль центра. Удобно, но вместе с ними переехали и русские буквы Х и Ъ. Набирать их со второго слоя очень медленно.



В прошивке можно сделать так, чтобы короткое нажатие на левый shift набирало [, а на правый ], так нашлось место для Х и Ъ. Появилась новая проблема: чтобы набрать заглавную Х, надо нажать правый shift, а потом нажать левый. Для Ъ наоборот, но это приходится делать нечасто. Также можно набирать { и }.



Пользоваться правым shift я не привык, 90% опрошенных тоже. Пришлось попробовать и мне понравилось! Ломает голову, но разгружает руки.

Софт QMK


В клавиатуре крутится софт QMK quantum mechanical keyboard. Опенсорс, широко поддерживается, красота. Настраивать клавиатуру можно через конструктор на сайте или перепрошивать через терминал.

Конфигуратор на сайте


Сделать раскладку можно на сайте-конфигураторе config.qmk.fm.

Работает очень просто: перетаскиваете кнопки снизу наверх, жмёте скомпилировать (Compile), скачиваете (Firmware).



Через программу QMK Toolbox обновляете прошивку: выбираете файл, жмёте flash, нажимаете кнопку сброса на клавиатуре. Готово. У клавиатуры Iris кнопки сброса есть снизу платы, но можно повесить и на обычную кнопку в каком-нибудь слое.


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

Сборка на компе


Возможностей конфигуратора хватает для настройки кнопок, слоёв, но не работает с крутилками, пришлось устанавливать сам QMK. Гайд объяснит всё даже тем, кто с командной строкой не работал.

Для установки QMK надо выполнить 4 команды
Установить brew:
brew install qmk/qmk/qmk

Запустить настройку:
qmk setup

Дать название вашей раскладке:
qmk config user.keyboard=clueboard/66/rev4
qmk config user.keymap=<github_username>


После установки у вас появится файл с вашей раскладкой.



Список клавиш можно найти на сайте QMK.

Если что-то поменяли, то достаточно выполнить:

qmk compile // скомпилирует вашу раскладкуqmk flash// загрузит раскладку в клавиатуру 

И потом нажать на клавиатуре кнопку reset.

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

void encoder_update_user(uint8_t index, bool clockwise) {    if (index == 1) { // Right encoder        if (clockwise) {            tap_code(KC_VOLU);        } else {            tap_code(KC_VOLD);        }    }}

Можно усложнить действия, добавив слои
void encoder_update_user(uint8_t index, bool clockwise) {    if (index == 1) { // Right encoder        if (layer_state_cmp(layer_state, _LOWER)) {            if (clockwise) { // Стрелки вверх-вниз                tap_code(KC_DOWN);            } else {                tap_code(KC_UP);            }        } else if (layer_state_cmp(layer_state, _RAISE)) {            if (clockwise) { // Громкость                tap_code(KC_VOLU);            } else {                tap_code(KC_VOLD);            }        } else {            if (clockwise) { // Стрелки влево-вправо                tap_code(KC_RGHT);            } else {                tap_code(KC_LEFT);            }        }    }}


Что можно сделать со своей прошивкой?


В QMK много необычных возможностей. Три важных мы уже разобрали: слои, поддержка сплит клавиатур и rotary encoder (это так крутилки называются). Но внутри много интересного. Расскажу про парочку необычных примеров, что я использовал.

Space cadet shift


В обычной клавиатуре shift или alt работают только на зажатие. В QMK им можно добавить действие и на нажатие. Например, левый shift будет писать открывающую [, а правый закрывающую ]. Ещё я так настроил аlt/enter.

Макросы


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

Пара примеров. Обновление прошивки двумя командами можно свести до одной клавиши: вводим qmk compile, ждём пару секунд пока выполнится, вводим qmk flash и программно жмём кнопку reset. Весь процесс свёлся до одной клавиши.

Хоба
SEND_STRING("qmk compile\n");SEND_STRING(SS_DELAY(2000));SEND_STRING("qmk flash\n");reset_keyboard();


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

var  SomeClassMock: SomeClassInterfaceProtocol!

Код для этого
// write var ClassNameMock: ClassNameMockSEND_STRING("var "); SEND_STRING(SS_LGUI("v"));  // Past from bufferSEND_STRING("Mock: "); SEND_STRING(SS_LGUI("v"));  // Past from bufferSEND_STRING("Mock");


Не получится уменьшить первую букву переменной, но можно переместить до неё курсор (alt + влево два раза) и удалить (через del), останется только ввести букву самому.

Дописываем
SEND_STRING(SS_LALT(SS_TAP(X_LEFT)));SEND_STRING(SS_LALT(SS_TAP(X_LEFT)));SEND_STRING(SS_TAP(X_DEL));


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

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

someClassInterfaceMock = SomeClassInterface()

А затем и сгенерировать шаблон для мока в Spry:

сlass SomeClassInterfaceMock: SomeClassInterfaceProtocol: Spryify {    enum ClassFunction: String, StringRepresentable {        case empty    }        enum Function: String, StringRepresentable {        case <#empty#>    }}

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


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

Применений макросам много, надо просто поискать паттерны в своей работе.
Есть и другие фишки, но я их ещё не пробовал: auto shift, combos, tap dance, leader key и кучу всего привязанного к железу.

Итог


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

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

Есть и сложности. К новой раскладке привыкать не очень сложно, а вот хоткеи могут значительно поменяться и стать сложнее. Я стал упрощать, как сами хоткеи (cmd + shift + G для запуска тестов в Xcode это глупость), так и поменял раскладку, чтобы было удобнее нажимать их. 60% клавиатуры хороший компромисс для тех, кто не готов пожертвовать цифрами, как в 40% клавиатуре.

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

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

Не бойтесь пробовать и менять привычные инструменты!
Планы на будущее: уже заказал и хочу попробовать пару других клавиатур. Это Atreus, он недавно вышел с кикстартера, и скоро начнётся нормальная продажа, и 40% Jian с нормальной русской раскладкой, но на него надо успевать записываться, групбай почти закончился.

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

Путь разработчика в 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, подписывайтесь на рассылку и следите за публикациями в блоге.
Подробнее..

Анимация в 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 проверьте, нет ли у вас таких проблем.
Подробнее..

Спасибо за собеседование, мы ответим о нашем решении сейчас

18.06.2020 20:20:38 | Автор: admin
Когда я сам был кандидатом и ходил по собеседованиям, больше всего меня бесило ожидание обратной связи: долго, скучно, нельзя обсудить решение. Оказавшись на месте интервьюера, я заметил, что чаще всего все нужные выводы делаются буквально за 5 минут после встречи. Остальное время бесполезное растягивание процесса и бюрократия. Главная причина не отвечать сразу понятна эмоционально сложно обсуждать решение с кандидатом, ведь часто нужно отказывать. В итоге программисты увиливают и передают эту задачу HR.

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




Серия статей про собеседования:
1. Я прочитал 80 резюме, у меня есть вопросы.
2. Наш первый обед вместе: почему и как мы проводим тестовый день.
3. Собеседование в Додо Пиццу.
4. Уходя уходи: почему не стоит принимать контроффер.

Я разрабатываю приложения для айфонов, но ещё провожу технический этап собеседования для iOS-разработчиков.

Раньше я проводил собеседование по стандартной схеме:

  • слушал рассказ кандидата о себе;
  • задавал вопросы;
  • рассказывал о следующем этапе собеседования;
  • договаривался о времени ответа и мы прощались.

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

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


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

Сейчас я провожу собеседование так:

  1. Рассказываю о структуре собеседования. Буквально то, что вы сейчас читаете.
  2. Рассказываю кратко про наш проект, его интересные стороны и будущие вызовы, про команду. Отвечаю на вопросы, которые могут появиться.
  3. Кандидат рассказывает про свой опыт, а я попутно записываю вопросы. Спрашиваю всё в конце, чтобы не перебивать.
  4. Если какую-то тему не обсудили, то спрашиваю точечно. Чтобы ничего не забыть, беру с собой на встречу чеклист из тем и вопросов.
  5. Часто к концу встречи у кандидата снова появляются вопросы. Отвечаю на них.
  6. В конце даю фидбек: принимаю решение на своём этапе технического собеседования и рассказываю о нём.
  7. Раньше я думал, что на шестом этапе собеседование заканчивается, но у меня раз за разом происходила магия: после фидбека (неважно, положительного или отрицательного) кандидаты преображались, рассказывали необычные детали о себе и своём опыте, раскрывались с новой стороны.

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

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

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


До фидбека


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

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

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

Чеклист вопросов и тем, чтобы ничего не забыть. Не пропустить какую-то тему мне помогает чеклист. Обычно процентов 60% из него кандидат рассказывает сам. Задавая дополнительные вопросы, стараюсь отталкиваться от рассказа самого человека, чтобы быть в мире собеседника, он там лучше разбирается. Получается нормальное общение.

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

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

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

Во время фидбека


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

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

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

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

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

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

После фидбека


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

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

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

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

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

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

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

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

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

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

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

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

Итоги и выводы


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

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

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

Если в вашей компании открытость и честность это часть культуры, то почему бы не начать прямо на интервью?
Свои мысли и идеи о мобильной разработке я пишу в телеграм-канале Dodo Pizza Mobile. А ещё у нас открыто две вакансии в мобильном направлении. Так что я просто оставлю это здесь: iOS-developer (Нижний Новгород), Android-developer (Нижний Новгород).
Подробнее..

Категории

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

  • Имя: Макс
    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