![](https://habrastorage.org/getpro/habr/upload_files/0f2/047/71d/0f204771d67709550b7f8a2e9d7a877f.png)
Backend Driven UI (BDUI) — это концепция, при которой сервер управляет не только данными в приложении, но и формирует интерфейсы: экраны, верстку, реакции на взаимодействия пользователя и переходы между экранами. Задача клиентской стороны сводится к рендерингу экранов на основе данных, полученных с сервера.
На первый взгляд, это даёт такие преимущества:
Снижение Time to Market — сокращение времени от идеи до готовой фичи, доступной пользователям;
Создание UI для нескольких платформ одновременно;
Возможность обновлять функциональность без необходимости обновления приложения, включая старые версии у пользователей.
На бумаге такие идеи выглядят отлично, порой даже как «серебряная пуля». Однако на практике всё может быть не так просто — многое зависит от конкретного кейса. В этой статье я поделюсь реальным опытом нашей компании, расскажу о нюансах, благодаря которым удалось извлечь выгоду из этой концепции, и, конечно, представлю рабочий инструмент. В нашей реализации BDUI есть нестандартные подходы, которые, надеюсь, вдохновят и вас. Похоже, BDUI для нас — это больше удовольствие, чем боль. Давайте разберёмся, как это получилось!
Материал большой, поэтому держите оглавление:
Разработка
3.1 Инпуты
- Виджет
- Модификаторы
- Состояния
3.2 Взаимосвязанные элементы
- JS Action
3.3 Форматирование
3.4 Валидация
3.5 Переходы
Как мы пришли к BDUI
Для начала дам немного контекста о проекте. Циан — крупная PropTech компания. У нас работает более тысячи сотрудников в 12 продуктовых направлениях.
В мире мобильных приложений наш основной продукт — это приложение Циан, многофункциональный сервис для покупки, продажи и аренды недвижимости, включающий сопутствующие услуги. Каждый месяц сервисом пользуются 19 миллионов уникальных пользователей, а количество актуальных объявлений достигает почти 2 миллионов.
Функционала и экранов в нашем мобильном приложении действительно много, но до недавнего времени полноценная реализация BDUI вовсе отсутствовала.
Одной из ключевых фич нашего приложения является форма подачи объявления. Это важный инструмент, с помощью которого пользователи могут разместить объявление о продаже или аренде любого типа недвижимости. Главная продуктовая метрика здесь — конверсия в успешную публикацию.
Эта форма крайне важна для всех 12 наших продуктовых вертикалей, и каждая из них активно работает над её улучшением. Мы критически пересмотрели текущую реализацию и задали себе вопрос: «Помогает ли она нам развивать фичу или мешает?»
Первое, что сразу бросилось в глаза — это негативный User Experience.
![](https://habrastorage.org/getpro/habr/upload_files/174/d9f/ea1/174d9fea1f1e083dabb5404007ffe509.png)
Изначально форма подачи объявления была разработана много лет назад и с тех пор обрастала новыми полями, превращаясь в настоящий свиток, наш Elder Scroll. Дизайн практически не пересматривался, и с первого взгляда видно, что он застрял в прошлом десятилетии.
С технической стороны ситуация не лучше:
iOS: используется Objective-C
Android: Java
Веб: Angular 1.5 (актуальная версия на момент написания статьи — 18.0)
Несмотря на исследования, чёткой архитектуры мы не обнаружили. Всё это приводит к низкой стабильности продукта, множеству багов, высокой стоимости поддержки и напрягает команду разработки. Более того, это усложняет нам найм людей.
Особенно стоит отметить высокую стоимость поддержки из-за того, что логика размазана между клиентом и бэкендом. Для внесения изменений требуется участие разработчиков всех стеков: iOS, Android, фронтенд и бэкенд.
Ещё одна проблема — поддержка старых версий приложений. Она особенно ощущается, когда нужно добавить новое обязательное поле. Это сразу вызывает головную боль: как сделать обязательное поле, не нарушив работу пользователей старых версий? Решить проблему значением по умолчанию в базе данных не всегда возможно.
Итог такой: чтобы внести доработку, нужно задействовать минимум четырёх разработчиков, а работы идут долго. Грустно, конечно…
А хочется совсем других показателей:
![](https://habrastorage.org/getpro/habr/upload_files/c0e/e2e/369/c0ee2e369fa20b2f1736886ab682e68b.png)
Есть над чем поработать!
Выбор реализации
Проблемы ясны — первый шаг сделан. Теперь можно составлять требования.
Мы хотим новую форму подачи, у которой будет:
Современный стек технологий;
Предсказуемость оценок;
Оптимальная стоимость изменений и поддержки;
Высокий Bus Factor.
При должной реализации эти требования можно достичь разными путями.
Однако есть и другие важные вещи:
Консистентный UI;
Синхронизация логики;
Простота проведения A/B экспериментов;
Ускорение релизного цикла.
Для их достижения требуется особый подход. Мы рассмотрели несколько вариантов: новую нативную реализацию, WebView и BDUI. Ниже приложу их сравнение.
Новая нативная реализация |
WebView |
BDUI |
✅ Нативный UX |
⛔️ Ненативный UX |
✅ Нативный UX |
⛔️ 3 человека на фичу |
2 человека на фичу |
✅ 1 человек на фичу |
⛔️ TTM 2 недели |
✅ TTM 1 день |
✅ TTM 1 день |
Сразу отбросили вариант с WebView из-за его ненативного UX. А вот BDUI показался гораздо более перспективным по сравнению с традиционной нативной разработкой, так как его преимущества соответствовали бизнес-требованиям для формы подачи. Однако BDUI дорог на начальном этапе, поэтому первым делом мы обсудили этот подход с бизнесом.
![](https://habrastorage.org/getpro/habr/upload_files/b03/e37/903/b03e37903d5d6ffdd595b652228c6215.png)
Понимая, что старт будет дорогим, мы договорились не ограничиваться решением только для формы подачи, а создать универсальный механизм, который будет полезен всем продуктовым вертикалям. Для консистентности UI решили использовать нашу дизайн-систему. Реализация должна была быть универсальной в рамках компании, адаптированной под наши продукты и процессы.
Ну вот, чем не требования? Осталось всего лишь реализовать это — сущая ерунда! (спойлер: как бы не так)
Разработка
И вот глянули мы на новые макеты:
![](https://habrastorage.org/getpro/habr/upload_files/495/651/530/495651530bf14cc7d89751b3594f2242.png)
Как и любая форма, наша включает много инпутов. Некоторые из этих элементов взаимосвязаны между собой, включая сами инпуты. По стандарту есть и текстовые поля с поддержкой форматирования.
![](https://habrastorage.org/getpro/habr/upload_files/c38/e65/9fc/c38e659fc787e326b3f95d6de2c283b5.png)
Конечно, без валидации никуда. В отличие от старой версии, где всё было свитком на одном экране, новая форма подачи получила разбивку на логические шаги, то есть теперь она включает в себя переходы между экранами.
![](https://habrastorage.org/getpro/habr/upload_files/8f0/3d6/462/8f03d64629cb09639b5d24d5e9211521.png)
Итак, у нас есть:
Инпуты
Взаимосвязанные элементы
Форматирование
Валидация
Переходы
Давайте посмотрим, как мы со всем этим справились.
Инпуты
Все элементы UI у нас называются виджетами, и инпут — не исключение.
На верхнем уровне есть три основные сущности:
Виджет
Модификатор
Экшен
В нашем контракте с бэком всё это описывается в формате JSON.
![](https://habrastorage.org/getpro/habr/upload_files/e47/d0c/e51/e47d0ce513923588bd61df93b792155c.png)
Виджеты
Виджет — это элемент UI.
![](https://habrastorage.org/getpro/habr/upload_files/b84/cb3/064/b84cb3064ebc0062017e859b53685797.png)
В общем его формате присутствуют:
ID;
type — его тип;
params — у каждого типа виджета свой набор параметров, которые его настраивают. Из общего в params можно выделить модификаторы, но о них позже;
У некоторых виджетов могут быть children, но не у всех.
Перед вами пример текстового поля, JSON:
![](https://habrastorage.org/getpro/habr/upload_files/dd2/b8d/935/dd2b8d935e256d65474f0848cdfd38bf.png)
Справа вы можете увидеть полученное отображение этого описания.
Наш инструмент включает библиотеку стандартных виджетов, которые можно разделить на два типа: атомарные и контейнеры.
![](https://habrastorage.org/getpro/habr/upload_files/9b1/6f4/cff/9b16f4cffa146c3b376859285ddb35bf.png)
Контейнеры поддерживают children.
Атомарные неразрывно связаны с нашими компонентами дизайн-системы.
![](https://habrastorage.org/getpro/habr/upload_files/940/c2a/bc0/940c2abc0538f1456731c216b141bcd8.png)
Дизайн-система, конечно, сильно выручила нас в создании виджетов. У виджетов по сути уже была готовая спецификация, которая определила контракт. Реализация атомарных виджетов свелась к обёртке над компонентами дизайн-системы, что означало, что большая часть работы уже была сделана, причём с учётом уже отлаженных edge-кейсов.
Дизайн-система также помогает при создании новых фичей за счёт гайдлайнов, и фичи на BDUI — не исключение.
Модификаторы
К виджетам могут применяться модификаторы.
Логика нашего контракта очень близка к терминам Compose.
![](https://habrastorage.org/getpro/habr/upload_files/d0c/695/d1f/d0c695d1fdb374d6dadda0436f123d63.png)
По формату у модификаторов есть:
name — кодовое название;
value — значение;
params — дополнительные параметры, которые зависят от конкретного type;
type — сообщает нам, какого типа ожидается value и params, что помогает при парсинге.
Например, чтобы зафиксировать ширину нашего текстового поля, к нему нужно применить модификатор.
![](https://habrastorage.org/getpro/habr/upload_files/85e/b1f/fbc/85eb1ffbc8b73cdef7e0c3c79d2c6343.png)
Есть следующие группы модификаторов:
![](https://habrastorage.org/getpro/habr/upload_files/79a/56b/f72/79a56bf7207c1ea8ffe8347719dd2cd3.png)
Интерактива (userInteractionEnabled);
Верстки, рассмотренный модификатор ширины попадает как раз в эту группу;
Состояния отображения (visible, alpha и т.д.);
Стиля отображения, куда попадают такие настройки, как backgroundColor, clipShape, shadow, border и т.д. Эту группу поддерживают не все виджеты, что продиктовано нашей дизайн-системой.
Состояния
У каждого виджета есть поле state — JSON-объект с его состоянием.
Туда могут быть вынесены значения любых параметров, которые могут динамически измениться.
Все настройки виджета являются generic-типом StatedValue:
С помощью его поля value значение зашивается;
С помощью поля state значение берется из состояния.
Как видно в примере, у title зашито значение «Общая площадь», в то время как поле text изменяемое.
![](https://habrastorage.org/getpro/habr/upload_files/c01/d33/023/c01d330234dfb3452d363487db55798b.png)
У любого виджета есть доступ к своему state’у и state’у самого корневого виджета, который является глобальным.
Вот так у нас описываются инпуты.
Взаимосвязанные элементы
Давайте посмотрим, как реализованы взаимосвязанные элементы.
Это имеет прямое отношение к экшенам.
![](https://habrastorage.org/getpro/habr/upload_files/174/4e8/0bd/1744e80bdf5af948453942fe4cd737fd.png)
В общем формате action обязательно присутствует:
actionData — данные для выполнения экшена;
type — экшены у нас бывают двух типов, JS и event;
fallbackActions — к этому полю мы вернемся позже.
JS Action
JS Action — это экшен, исполняющий код JavaScript.
![](https://habrastorage.org/getpro/habr/upload_files/5bd/7bd/eb1/5bd7bdeb18f76b2489b63306f64c7043.png)
Составляющие аctionData:
jsCode — код с main функцией, которая и будет выполнена;
targetState — состояние, которая должна эта функция изменить:
- none — никакое, выполнится просто как void-функция;
- в случае local функция меняет состояние текущего виджета;
- в случае global — корневого виджета.
Вернемся к нашему текстовому полю, где в state есть предустановленное значение 100.
![](https://habrastorage.org/getpro/habr/upload_files/602/b9c/68a/602b9c68a2b119ea4f257f2d81fd9d91.png)
Что здесь происходит: мы описали JS-экшен, который срабатывает при изменении текста пользователем и заменяет значение totalArea в state на то, что вводит пользователь с клавиатуры.
А что происходит во взаимосвязанных элементах:
Состояние элемента зависит от действий;
От состояния одного элемента зависит состояние другого.
![](https://habrastorage.org/getpro/habr/upload_files/e46/44a/06c/e4644a06c301c51d0787ba2a2c26cb8e.jpg)
Посмотрим как реализовать такой выпадающий текст.
![](https://habrastorage.org/getpro/habr/upload_files/6c3/d33/d36/6c3d33d36072625311fb449e27c02646.jpg)
При нажатии на кнопку выполняется JSAction, который изменяет значение булевого поля infoExtended в глобальном стейте. У элемента, который отображает текст описания, есть модификатор видимости, связанный с этим полем infoExtended. В результате происходит смена состояния: нажатие на кнопку приводит к появлению или скрытию текста, как показано на картинке выше. По такому принципу можно построить любую взаимосвязь между элементами.
Теперь давайте рассмотрим диаграмму последовательности, чтобы понять, что происходит во время выполнения JSAction:
![](https://habrastorage.org/getpro/habr/upload_files/e2c/8fc/ddb/e2c8fcddb2019f21afc283ac01671e12.png)
Пользователь нажимает на кнопку, это действие активирует JS Engine, который выполняет JavaScript-код для изменения состояния. На эти изменения подписана фабрика виджетов, которая через JSON path получает обновлённую конфигурацию виджетов и обновляет UI.
Таким образом, связка JS и state создаёт универсальный механизм. Весь JS-код присылается с бэка, а на клиентах остаётся минимум логики, что позволяет вносить изменения без необходимости правок на стороне клиентов.
Мы увидели преимущества использования JavaScript перед более традиционными подходами с Conditions или регулярками. Регулярные выражения могут быть встроены в JavaScript-код на стороне бэка, что упрощает управление логикой.
Форматирование
Прежде всего, оно необходимо для текстовых инпутов. Форматирование выполняет две задачи: позволяет вводить только определённые символы и корректирует текст под нужный способ отображения.
В нашем приложении особенно часто требуется форматирование цены и номера телефона. Эти форматтеры реализованы на уровне дизайн-системы и вынесены в контракте BDUI в отдельное поле inputFormatter типа enum. Вот некоторые из его значений:
price — форматирование цены;
priceFractional — цена в рублях с копейками;
phone — форматирование номера телефона.
Если нужны другие, кастомные форматтеры, нас снова выручает JavaScript.
![](https://habrastorage.org/getpro/habr/upload_files/e56/463/a89/e56463a89418f92526d158464dc3d706.png)
Для этого мы создали отдельное поле onTextChanged, в котором содержится текст JS-функции. Эта функция принимает текст в качестве аргумента и возвращает отформатированную строку. Такая гибкость в использовании форматтеров действительно необходима, например, в случае поля ввода кадастрового номера.
![](https://habrastorage.org/getpro/habr/upload_files/49b/cd9/e3f/49bcd9e3f6c001f3cee617374175376f.png)
Валидация
Схема использования такая: пользователь нажимает на кнопку «Продолжить».
Если не все поля заполнены правильно, нужно подсветить ошибки;
Если всё в порядке, то переходим на следующий шаг.
![](https://habrastorage.org/getpro/habr/upload_files/c01/4a2/9f6/c014a29f67c2493e555295df3031329f.jpg)
Вот мы и подобрались ко второму типу экшенов: Event Action.
![](https://habrastorage.org/getpro/habr/upload_files/bb3/5a3/f3f/bb35a3f3f84cd693c93ad1d948023021.png)
Этот экшен выполняет заранее прописанный нативный код.
Здесь в actionData:
eventName — название события;
eventData — данные для выполнения;
fallbackActions — к этому полю вернемся позже).
В нашем инструменте есть готовый набор ивент экшенов:
![](https://habrastorage.org/getpro/habr/upload_files/565/bed/c7d/565bedc7de80c3727a8f26dfe62cddde.png)
actionFromLink нужен для запроса на бэкенд;
updateGlobalState — производит обновление глобального стейта;
updateFullJson полностью перестраивает UI, не только стейт;
newScreen и newScreenStack нужны для переходов на новые экраны;
back осуществляет возврат к предыдущему;
showInfo показывает информер;
sendAnalytics отправляет аналитику;
universalLink, как вы поняли, совершает переход по универсальной ссылке.
Для каждого из этих типов в eventData содержится своя информация. Нам нужна валидация на стороне бэкенда, поэтому, когда пользователь нажимает на кнопку «Продолжить», нужно выполнить запрос и показать состояние загрузки. Запрос выполняется с помощью ивент-экшена actionFromLink, передавая URL в eventData.
{
"type": "event",
"actionData": {
"eventName": "actionFromLink",
"eventData": {
"link": "https://api.cian.ru/…/next-step"
}
}
}
Показ состояния загрузки — через ивент экшен updateGlobalState, в eventData новые значения стейта.
{
"type": "event",
"actionData": {
"eventName": "updateGlobalState",
"eventData": {
"unionStrategy": "merge",
"newState": {
"enabled": false,
"loading": true
}
}
}
}
Самое время упомянуть, что у нас предусмотрен встроенный механизм клиент-серверного взаимодействия. Он построен на рекурсивном полиморфном контракте, что вполне логично, учитывая, что виджеты выстраиваются в иерархию.
![](https://habrastorage.org/getpro/habr/upload_files/423/9ca/42e/4239ca42ef8562eb465da381addba0b8.png)
У всех запросов есть единый формат: на сервер передаётся весь JSON-объект globalState, а в ответ приходит массив экшенов для выполнения. Если не все поля заполнены правильно, нам нужно подсветить ошибки.
![](https://habrastorage.org/getpro/habr/upload_files/a48/35a/07a/a4835a07a4673f3c19eb5f2540adac3e.png)
Поэтому сервер нам присылает Event Action обновить стейт.
{
"actions": [{
"type": "event",
"actionData": {
"eventName": "updateGlobalState",
"eventData": {
"unionStrategy": "merge",
"newState": {
"enabled": true,
"loading": false,
"totalAreaError": "Заполните площадь"
}
}
}
}]
}
В этом стейте помимо сброса состояния загрузки присутствует текст для ошибки поля «Общая площадь». А сам текст инпут ссылается на это поле из globalState.
![](https://habrastorage.org/getpro/habr/upload_files/497/c08/fe0/497c08fe0986b8728dfc5bd0e257400c.png)
И вот какая красота получается в этом негативном сценарии:
![](https://habrastorage.org/getpro/habr/upload_files/5f1/dd4/77c/5f1dd477c356227cb7aa2ab0f05a0718.jpg)
При нажатии кнопки начинается состояние загрузки, которое продолжается до получения ответа на запрос. После ответа лоадер исчезает, а ошибочные поля подсвечиваются. Однако сценарий может развиваться еще хуже, ведь всегда существует риск ошибки интернет-соединения.
![](https://habrastorage.org/getpro/habr/upload_files/167/10d/2db/16710d2dbc31b844939d2e3b5aed3ef3.jpg)
В этом случае необходимо показать пользователю информер об ошибке и сбросить состояние загрузки. Но как это сделать без логики на клиенте? Ответ кроется в использовании экшенов. Здесь как раз на помощь приходит поле fallbackActions, к которому мы теперь и обратимся.
![](https://habrastorage.org/getpro/habr/upload_files/c1a/790/823/c1a7908235ea5a2e04e21a305538c60e.png)
Именно для таких случаев мы и предусмотрели их в контракте. Поле fallbackActions содержит список экшенов, которые выполняются, если при выполнении текущего экшена возникает ошибка. В данном случае ошибка сети — это и есть ошибка выполнения actionFromLink.
Соответственно, мы добавляем в fallbackActions два экшена: один, который отображает информер (showInfo), и второй, который сбрасывает состояние загрузки (updateGlobalState).
![](https://habrastorage.org/getpro/habr/upload_files/45c/02b/3be/45c02b3be71f41b8579a8147a6db8101.png)
И теперь ошибка соединения приведет к показу информера.
![](https://habrastorage.org/getpro/habr/upload_files/fab/f19/93a/fabf1993ad6a5e98437c59e4bfb76684.jpg)
Вот и с валидацией мы разобрались. Осталось рассмотреть переходы.
Переходы
Переход на следующий шаг формы подачи происходит по нажатию на кнопку «Продолжить» при позитивном сценарии валидации, когда все поля заполнены верно.
![](https://habrastorage.org/getpro/habr/upload_files/497/b8c/9fc/497b8c9fc0c4c4a50a3e8a452bc4aaca.png)
![](https://habrastorage.org/getpro/habr/upload_files/70a/46d/2de/70a46d2deb2995aacb4d79c85aabb4b7.jpg)
Здесь сервер нам просто возвращает экшен newScreen с уже готовым JSON’ом следующего экрана.
{
"actions": [{
"type": "event",
"actionData": {
"eventName": "newScreen",
"eventData": {
"stack": "horizontal",
"json": <JSON следующего экрана>
}
}
}]
}
И вот как это выглядит визуально:
![](https://habrastorage.org/getpro/habr/upload_files/9ad/db4/c57/9addb4c5747d262dbf3d5ea41029bc1c.jpg)
Нажатие на кнопку;
Запрос с загрузкой;
Стандартный переход.
Это переход с уже загруженным JSON на предыдущем шаге.
Но существует и вариант перехода без предзагруженных данных. В случае перехода с одного экрана BDUI на другой нужно в Event Action newScreen передать не готовый JSON, а ссылку, по которой будут загружаться данные. Если же мы создаем экран из привычного нативного кода, фабрика модуля также поддерживает в качестве входных параметров не только JSON, но и ссылку.
![](https://habrastorage.org/getpro/habr/upload_files/f52/dbc/846/f52dbc846c90f051015eadf598456d9e.png)
Рассмотрим диаграмму последовательности такого сценария.
![](https://habrastorage.org/getpro/habr/upload_files/7da/14b/462/7da14b46282398378a737402e1ea3923.jpg)
Сначала модуль совершает запрос по указанной ссылке, сервер возвращает JSON, описывающий экран, в экшене updateFullJson. Модуль обновляет стейт с полученными данными, а фабрика виджетов, подписанная на изменения стейта, по уже знакомой схеме через JSON path получает финальную конфигурацию виджетов и обновляет UI.
При загрузке сервер присылает нам вот такой Event Action updateFullJson:
![](https://habrastorage.org/getpro/habr/upload_files/519/44b/631/51944b631e5135ac8e87f2741b8b4845.png)
Как видно, eventData содержит JSON экрана. Этот экшен используется для первой отрисовки экрана, но может применяться и для последующей, когда одного updateGlobalState недостаточно и требуется пересобрать весь UI. Таким образом, мы рассмотрели все возможные переходы.
Вертолеты
Давайте взглянем на наш набор виджетов:
![](https://habrastorage.org/getpro/habr/upload_files/997/316/9b8/9973169b884e8d18d7ab48f3d2e6f621.jpg)
и на наш набор стандартных Event Action’ов:
![](https://habrastorage.org/getpro/habr/upload_files/942/150/4f0/9421504f0c2a824289baad2591a50540.jpg)
А вдруг этого не хватит?
На этот случай мы предусмотрели дополнительные возможности, которые назвали «вертолетами». Это компоненты, которые не входят в библиотеку стандартных компонентов BDUI, поскольку они не универсальны и заточены под конкретные продуктовые кейсы. Эти компоненты разрабатываются и поддерживаются продуктовыми командами. Хотя «вертолеты» не являются частью основного инструмента, их можно инжектировать в BDUI-модуль при его создании.
Вертолетами могут быть, например, виджеты. Для этого нужно проинжектировать фабрику реализации. При вызове метода создания BDUI-модуль передаст необходимые зависимости в качестве параметра.
![](https://habrastorage.org/getpro/habr/upload_files/1f5/217/8b0/1f52178b01aac39632e3ff6ae0fb37ec.png)
Вертолетами могут быть не только виджеты, но и ивент-экшены. Для этого нужно проинжектировать обработчик, который поддерживает соответствующий протокол. Эта возможность придает нашему инструменту дополнительную гибкость.
![](https://habrastorage.org/getpro/habr/upload_files/a5c/15d/d0d/a5c15dd0dc9a34ab7684b6880fa560ed.png)
Так мы в новой форме подачи использовали существующую реализацию формы выбора услуг.
![](https://habrastorage.org/getpro/habr/upload_files/b49/6cf/843/b496cf8435b06b98724bcbe1582315e7.jpg)
На бэкенде инжектируется мощный микросервис, а на фронте — экран совершенно несвязанного модуля. Такое решение позволило нам сэкономить массу времени.
Стратегия ошибок
Пару слов про стратегию обработки ошибок. Если в JSON описания экрана придет незнакомый виджет или экшен, это приведет к ошибке парсинга. Однако, если встретится незнакомый модификатор, он просто будет проигнорирован. Что касается полей, то битое или пустое обязательное поле также вызовет ошибку парсинга. Отсутствующее опциональное поле считается валидным кейсом, а вот битое опциональное поле — это ошибка.
Версионирование
Общая версия BDUI представлена в формате <major>.<minor> и формируется из совокупности версий Core-компонентов: виджетов, модификаторов и экшенов. Важно отметить, что вертолеты не влияют на версию BDUI. Мы поднимаем major версию в случае изменений, которые несовместимы с предыдущими версиями. Версии BDUI синхронизированы между iOS и Android.
Кроме основного подхода, у бэкенда продуктовых команд есть дополнительные возможности для управления версиями. Например, они могут ориентироваться на версию приложения или увеличить версию API-ручки. Эти возможности особенно полезны при работе с вертолетами, когда в одной версии приложения могут использоваться разные версии BDUI в различных фичах.
![](https://habrastorage.org/getpro/habr/upload_files/bcb/1b6/049/bcb1b6049c6b67ca14f80cf2c4a7e036.png)
Зачем мы так поступили? Причины две: у нас нет возможности синхронно обновлять все микросервисы на бэкенде, и таким образом мы снижаем риск повлиять на существующие фичи.
Как это работает? Реализация BDUI имеет единый фундамент для всего приложения, в который инжектируются реализации компонентов. В разных версиях BDUI могут использоваться разные реализации одного и того же компонента, как показано на схеме.
![](https://habrastorage.org/getpro/habr/upload_files/bb2/754/2bc/bb27542bc5a99b9c30f8cc27fc68627c.png)
Ретроспектива разработки
Если подытожить, то наш инструмент умеет:
Строить UI;
Валидировать;
Производить навигацию;
Отправлять аналитику;
Выстраивать клиент-серверное взаимодействие;
Дает возможность встраивать вертолеты в виде кастомных виджетов или кастомных экшенов.
BDUI-модуль может выступать как отдельным экраном, так и вьюшкой, встроенной в привычный нативный интерфейс.
Что касается технологий, мы используем Kotlin для Android и Swift для iOS (вместо Java и Objective-C в предыдущей реализации). UI в Android реализован декларативно с помощью Compose, а в iOS — через UIKit. Для выполнения JavaScript на Android используется движок V8, а на iOS — встроенный JavaScriptCore.
Чтобы упростить разработку и отладку, мы создали специальное приложение-песочницу, которое предоставляет удобные инструменты для разработки и тестирования конкретных фичей.
![](https://habrastorage.org/getpro/habr/upload_files/2a2/8f1/1a1/2a28f11a1cccc0d6e2a48c5fffcde339.jpg)
Этот инструмент позволяет разработчикам сконфигурировать виджет по макету и получить готовое JSON-описание для этой конфигурации. В процессе отладки целых модулей доступны следующие возможности:
Ввод JSON-описания в текстовое поле для просмотра построенного UI;
Вызов API для загрузки данных, загрузка описания из любого удаленного JSON-файла;
Указание локального файла при запуске на симуляторе устройства, что позволяет непрерывно перестраивать UI при его редактировании — своего рода Hot Reload.
Разработка большого проекта никогда не бывает простой, и, конечно, у нас тоже были боли. Для iOS одной из самых сложных задач было повторить реализацию поведения Compose. В итоге, нам пришлось добавить дополнительные поля в контракт, которые не используются в Android. У Android-версии нашей дизайн-системы было значительное отставание от iOS, что потребовало дополнительных усилий для синхронизации.
Однако наибольшие трудности возникли у Backend-разработки. Наш контракт построен на рекурсии и полиморфизме, а библиотеки, используемые на бэкенде, написаны под спецификацию OpenAPI 2.0, которая не поддерживает рекурсию и полиморфизм. Поэтому пришлось немало поработать, чтобы адаптировать систему. Кроме того, Backend-разработчики вышли на новый уровень Dev-тестинга, отлаживая UI. Из других сложностей можно отметить отсутствие готового механизма для тестирования JS-кода и значительное перераспределение работы в сторону бэкенда.
Несмотря на все трудности, создание BDUI привело к значительным улучшениям в коде и UX новой формы подачи. Пользователи также отметили эти изменения. Например, один из отзывов, переданный нашей командой исследователей, был таким: «Сейчас провожу интервью с риелтором. Я его спрашиваю про форму подачи, а он всё хвалит, что стало удобнее и понятнее. Подсказки там всякие появились.»
Метрики
Мы покрыли инструмент технической аналитикой и внимательно следим за метриками.
![](https://habrastorage.org/getpro/habr/upload_files/54b/5de/e88/54b5dee8833d55f0b42f266dc6f9d71c.png)
Построили графики для 99 перцентили, в которой:
124 мс — среднее время Time to Interactive экрана, то есть промежуток от действия, инициирующего открытие экрана до момента отображения экрана пользователю со всем загруженными данными;
5 мс — среднее время выполнения JS;
8,5 мс — среднее время обновления UI.
Эти цифры нас тоже радуют.
Если подвести итоги первых релизов, то можно отметить следующие достижения:
Мы перешли на актуальный стек технологий;
75% задач выполняются силами одного бэкенд-разработчика;
В 75% случаев задачи доходят до продакшена менее чем за один день, что существенно сократило Time to Market (TTM).
По продуктовым метрикам также наблюдается статистически значимый прирост конверсии в публикацию — с 61% до 64%. Хотя этот прирост не такой заметный, как сокращение TTM, это только начало. У нас есть все возможности для дальнейших доработок фичи.
Таким образом, мы считаем, что наши промежуточные цели достигнуты, и для нас BDUI скорее удовольствие, чем боль.
Заключение
BDUI хорошо подошёл для нашего приложения. Мы использовали его для новой формы подачи и фильтров и теперь рассматриваем для других фич, анализируя функционал и взаимодействуя с продуктовыми командами. BDUI особенно полезен, когда важны быстрый Time to Market, актуальность без обновления и частые эксперименты, но при нестандартном или сложном UI стоит подумать дважды.
Большая часть нюансов связана с бэкендом, куда переносится практически вся логика, что существенно перераспределяет задачи. Этот шаг раскрывает возможности BDUI, но просто перенести правила верстки недостаточно — потребуется постоянное обновление на клиенте. Вынос логики позволил нам охватить множество кейсов.
Мы не видим BDUI как средство удешевления разработки. Хотя он может сократить время для типовых задач, это сложный инструмент в разработке, тестировании и документации, с высоким порогом входа. Поэтому BDUI подходит не для всех задач, а для нас он в первую очередь — средство сокращения TTM.
Главный вывод: глобальные архитектурные новинки требуют тщательной проработки на старте. Мы довольны выбранным путём, но не планируем переводить всё приложение на BDUI. Архитектуру нужно выбирать под конкретный продукт или фичу, так как каждый инструмент хорош в своей области.
ruomserg
Круто! Несколько лет назад СБЕР презентовал свое решение, где они все новые фичи проталкивали на мобилку примерно вашим же способом - через generic виджеты. Потом, когда фича устаканивалась - в новую версию приложения добавлялся нативный комплексный виджет (например, для того чтобы интерактивно выбирать поля из классификаторов).
Но ваша система виджетов умеет делать вещи, которые сбер в своей презентации не показывал...
Jedr Автор
Привет! Спасибо за интерес к статье. Здорово, что у тебя был опыт с похожими решениями. Мы вдохновлялись существующими подходами, но постарались сделать нашу систему виджетов максимально гибкой и расширяемой. У нее действительно есть особенности, которые, как ты заметил, не всегда встречаются в других аналогичных решениях. Будет интересно услышать твое мнение, какие именно аспекты кажутся наиболее полезными