Представьте, что вы можете полностью изменить интерфейс мобильного приложения, не дожидаясь апдейта в App Store или Google Play. Новые кнопки, секции, экспериментальные UI — всё обновляется мгновенно, прямо с бэкенда. Это не магия и не футуристическая технология будущего, а реальность, которую мы внедрили в Яндекс Еде с помощью Backend-Driven UI.
Этот подход кардинально изменил процесс разработки мобильных интерфейсов, позволив нам быстрее внедрять новые фичи и гибко адаптироваться под потребности пользователей. Меня зовут Никита Шумский, я мобильный разработчик в Яндекс Еде. Сегодня я расскажу вам, как мы используем BDUI в наших мобильных приложениях, какие преимущества этот подход даёт нам в работе и с какими сложностями приходится сталкиваться.
А так ли вообще нужен BDUI?
Может, нативных подходов вполне достаточно? Ведь классическая мобильная разработка проверена временем, даёт полный контроль над UI и обеспечивает нативный пользовательский опыт. WebView, в свою очередь, позволяет обновлять интерфейс без публикации нового билда. Тогда зачем изобретать велосипед? Ответ прост: нативная разработка в больших продуктах слишком медленная, а WebView слишком ограничен. Но, давайте разбираться по порядку.

1. Дублирование кода и ресурсы на поддержку
Сегодня основная часть мобильной разработки сосредоточена вокруг двух основных платформ — Android и iOS. Даже если бизнес-логика одинакова, UI всё равно приходится делать дважды, учитывая особенности каждого стека. Это приводит к дополнительным расходам и времени, и денег. При этом время от времени появляются новые операционные системы — например, китайская HarmonyOS и российская «Аврора». В будущем их доля может вырасти, и поддерживать приложения может стать ещё сложнее.
А если в продукте много A/B-тестов, маркетинговых акций и динамически изменяемых экранов, обновлять их через классическую разработку становится совсем сложно.
2. Ограничения WebView
Некоторые компании пытаются обойти проблему дублирования кода через WebView, загружая UI как веб-страницу внутри приложения. Это позволяет обновлять интерфейс в реальном времени, но создаёт ряд новых проблем:
- Производительность хуже, чем у нативного кода. Анимации, плавность скролла, время отклика — всё это страдает.
- Ограничен доступ к нативным API. Работа с жестами, камерами, push-уведомлениями и другими возможностями устройства становится сложнее.
- UX ощущается не таким плавным. Даже самые проработанные WebView-интерфейсы ощущаются не так плавно, как нативные.
Мы тоже пытались экспериментировать с WebView в Яндекс Еде. Подробнее об этом вы можете посмотреть в видео доклада Константина Ларгина, руководителя Android-разработки Яндекс Еды. Но в итоге поняли, что пользователи заказывают меньше, если критически важные экраны рендерились через WebView.
3. Медленные релизы и долгий цикл обновлений
После каждого изменения UI в классической мобильной разработке нужно выпускать новую версию приложения. А это значит, что мы снова и снова повторяем весь цикл:
- Кодим и тестируем.
- Делаем код-ревью и правим баги.
- Отправляем приложение в App Store и Google Play.
- Ждём одобрения (от нескольких часов до нескольких дней).
- Постепенно раскатываем и мониторим метрики.
Из-за этого даже небольшие правки — типа подвинуть кнопку на пару пикселей вправо или изменить порядок блоков на главном экране — могут доходить до пользователей неделями.
В итоге получается, что классическая нативная разработка — как автомобиль на механике — даёт полный контроль, но требует больше времени и ресурсов. WebView решает проблему лишь отчасти и при этом ещё и ухудшает UX. Мы долго искали подход, который смог бы нам дать лучшее из обоих миров. И, посмотрев на опыт наших коллег из маркета, решили попробовать внедрить у себя BDUI.
Архитектура BDUI в Яндекс Еде
Как вы уже поняли, BDUI — это не просто ещё один инструмент или набор библиотек, а новый подход к разработке мобильных интерфейсов, позволяющий полностью формировать логику UI на бэкенде со всеми мапперами, форматерами и вёрсткой. На первый взгляд звучит здорово. Но давайте рассмотрим подробнее, как это всё работает на примере нашего продукта.

1. Layout-конфигуратор (CMS)
Прежде чем данные попадут на клиент, их нужно правильно структурировать. За это отвечает Layout-конфигуратор — наша внутренняя CMS-система, в которой конфигурируется UI.
По сути это то место, где мы собираем вместе все компоненты и определяем, как они должны выглядеть на экране. Тут мы можем задавать шаблоны страниц, добавлять виджеты и управлять их настройками, запускать A/B-тесты и менять контент почти в реальном времени.
Например, если нужно добавить новый рекламный баннер на главную страницу, мы просто добавляем новый виджет в Layout-конфигураторе. Никаких изменений в коде приложения, никаких обновлений в сторах — баннер появится у пользователей сразу после сохранения изменений в CMS.
Layout-конфигуратор использует YAML-файлы для настройки UI, в которых указываются:
- Виджеты и их параметры.
- Источники данных для каждого элемента.
- Логика отображения разных версий интерфейса в зависимости от условий.
Кроме того там есть виджеты, которые можно полностью настраивать через графический интерфейс, без необходимости писать код. Эти виджеты могут сами получать данные из указанных источников и передавать их в payload.
Layout-конструктор это сервис, который получает из Layout-конфигуратора список виджетов и далее делает необходимые запросы в различные сервисы, чтобы собирать все необходимые данные. Он же определяет достаточность данных для отрисовки виджета, занимается фолбеками, обработкой ошибок.
2. Мобильный бэкенд
Это главный элемент всей архитектуры BDUI. Его задача — собрать всю нужную информацию, превратить ее в UI-разметку и отправить на клиент.
Наш мобильный бэкенд написан на Kotlin + Spring. Он общается с внешними сервисами через Retrofit и использует DivKit — собственную библиотеку Яндекса для описания UI. Это бесплатная Open Source библиотека, которую может использовать любой желающий.
Для обработки запросов и формирования страниц мобильный бэкенд использует «Товарищ SDK» — нашу внутреннюю библиотеку. Она организует обработку данных в виде пайплайна из нескольких этапов:
- Drafting — создаёт черновик страницы, определяя, какие секции и элементы экрана должны быть показаны.
- Fetching — делает запросы в другие сервисы для получения необходимых данных.
- Rendering — компилирует JSON в формате DivKit вёрстки с полной разметкой экрана, который затем передаётся на мобильные устройства.
Как это работает на практике? Представим, что пользователь открывает в мобильном приложении экран списка магазинов. Запрос приходит на мобильный бэкенд. Мобильный бэкенд обращается к Layout-конструктору. Layout-конструктор забирает список виджетов из Layout-конфигуратора и далее делает запросы в несколько сервисов: одному за списком магазинов, другому за пользовательскими рекомендациями, третьему за промоакциями. Полученные данные из Layout-конструктора компонуются на мобильном бекенде в JSON-структуру, где с помощью DivKit прописаны элементы интерфейса: карточки, списки, заголовки, кнопки. Далее этот JSON отправляется обратно в мобильное приложение.
3. DivKit: рендеринг UI на клиенте
DivKit — это ключевая библиотека, отвечающая за рендеринг UI. Она принимает JSON-описание экранов, полученное от мобильного бэкенда, и превращает его в нативные UI-компоненты.
Работает это так: DivKit получает JSON-ответ от мобильного бэкенда, разбирает его и создаёт на его основе иерархию UI-компонентов. В отличие от WebView, тут нет рендеринга HTML — библиотека использует нативные View-компоненты Android и iOS. Это значит, что интерфейс, созданный через BDUI, работает с той же производительностью, что и обычный нативный UI, но при этом его можно менять мгновенно.
DivKit позволяет описывать интерфейсы в декларативном формате. Через JSON можно управлять анимациями, адаптивными параметрами и динамически менять компоненты. Кроме того, в JSON задаётся навигация, запуск камеры или карты, отправка аналитики и другие нативные действия. Это снижает нагрузку на мобильных разработчиков и упрощает работу с UI.
Роль Flex SDK в процессе рендеринга
«Flex SDK» — это библиотека, работающая в связке с DivKit и отвечающая за подготовку данных перед их рендерингом. Её основная задача — разбор JSON-ответов мобильного бэкенда и их трансформация в удобный формат для работы с DivKit.
С помощью «Flex SDK» мы обеспечиваем гибкость при работе с DivKit и можем вносить тонкие настройки в поведение компонентов, избегая излишней нагрузки на мобильный бэкенд. Благодаря такой связке нам удаётся добиться плавного и предсказуемого отображения UI даже при сложных сценариях.
Рендер UI на мобильном бэкенде шаг за шагом
На уровне архитектуры мобильного бекенда весь процесс рендеринга можно представить следующим образом:
1. Запрос от мобильного клиента
Мобильное приложение формирует запрос к мобильному бэкенду. Этот запрос может быть инициирован различными событиями: открытие экрана, действие пользователя, получение push-уведомления и т. д.
«Flex SDK» упаковывает запрос в определённый формат, который будет понятен «Товарищ SDK» на стороне бэкенда. Этот формат, по сути — стандартизированное API между клиентской и серверной частями BDUI.
Запрос содержит информацию, необходимую для идентификации нужного экрана и его состояния:
- Тип экрана (например, «главный экран», «экран ресторана», «корзина»).
- Идентификатор пользователя (если требуется).
- Параметры экрана (например, ID ресторана, поисковый запрос).
- Состояние экрана (например, прокрутка, введённый текст в поле поиска).
- Хедеры с информацией о версии приложения и платформе.
Пример body запроса экрана коллекций:
{
"request": {
"view": {
"slug": "shops_department",
"type": "collection"
}
},
"payload": {
"state": {
...
}
}
}
2. Мобильный бэкенд: входная точка
Мобильный бэкенд получает запрос и обрабатывает его с помощью «Товарищ SDK». Как уже упоминалось, «Товарищ SDK» организует обработку в виде pipeline, состоящего из интерцепторов.
Контроллеры экранов генерируем с помощью OpenAPI. Они имплементируют интерфейс ApiDelegate
, а также:
- Обрабатывают запросы, пришедшие от Flex SDK.
- Извлекают из запроса всю необходимую информацию, такую как тип экрана, параметры, состояние и т. д.
- Запускают запрос на формирование документа в pipeline.
@Component
open class CollectionApiService(
private val pipelineExecutor: PipelineExecutor,
) : CollectionApiDelegate {
override fun eatsV1BduiMobileV1CollectionPost(
collectionRequest: CollectionRequest
): ResponseEntity<Document> {
val document = pipelineExecutor.execute(collectionRequest)
return ResponseEntity(document, HttpStatus.OK)
}
}
3. Мобильный бэкенд: Drafting
Первым в pipeline срабатывает Draftsman
, так называемый DraftingInterceptor. Он:
- Создаёт «черновик» документа, который в итоге будет содержать полное описание UI. Этот черновик — ещё не JSON, а внутренняя структура данных «Товарищ SDK».
- Указывает, какие секции будут в документе.
- Описывает события ЖЦ контента и документа.
- Задаёт элементы отображения скафолда экрана (toolbar, navigation bar, background color).
- Настраивает отображение экрана и анимации.
- Передаёт черновик дальше по pipeline.
@Service
@BindTo(CollectionRequest::class)
class CollectionDraftsmen: TypedDraftsman<CollectionRequest, DraftingContext>() {
override suspend fun provideDraft(
context: DraftingContext,
request: DocumentRequest<CollectionRequest>
): DocumentDraft {
return DocumentDraft(
content = SectionContentDraft(
sections = listOf(
SearchBarSectionDraft(),
RteUpsellSectionDraft(legacyId = request.legacyId),
...
),
settings = SectionContent.Settings(
refreshable = true,
scrollable = true,
enableSnippetsAnimations = false,
),
lifecycle = CollectionLifecycleDraft(...),
),
lifecycle = CollectionDocumentLifecycleDraft(...),
scaffold = CollectionScaffoldDraft(...),
transitionSettings = TransitionSettings(
onShow = TransitionSettings.Options(...),
)
)
}
}
В блоке контента указываются черновики секций, имплементирующих интерфейс SectionDraft
. В них указываются дополнительные данные для фетчинга или рендеринга секции из входящего запроса.
class SearchBarSectionDraft() : SectionDraft
data class RteUpsellSectionDraft(
val legacyId: Long
) : SectionDraft
4. Мобильный бэкенд: Fetching
Для работы с черновиками секций используется SectionPlanner
. Он получает черновики секций, подготавливает для них Query запросы в вышестоящие сервисы и планирует очерёдность их исполнения. Выполняет преобразование сырых данных от сервисов в простые модельки RenderData
, содержащие минимально необходимые структуры данных для их отображения. В андроид такие называют ViewObject, а в iOS — ViewModel.
@Service
@BindTo(RteUpsellSectionDraft::class)
class RteUpsellSectionPlanner : SectionPlanner<RteUpsellSectionDraft, PlanningContext>() {
override fun plan(
draft: RteUpsellSectionDraft,
context: PlanningContext,
): Plan<RteUpsellWidgetRenderData> {
return Plan
.lets(RteUpsellQuery(draft.legacyId))
.done { response ->
RteUpsellWidgetRendererData(
imageUrl = response.imageUrl,
...
)
}
}
}
Для запроса данных для секций используются классы, имплементирующие интерфейс Query
.
data class RteUpsellQuery(
val legacyId: Long?,
) : Query<RteUpsellResponse>
Для работы с Query используется специальный интерсептор Fetcher
, это так называемый FetchingInterceptor. Он выполняет запросы к другим сервисам (продуктовым микросервисам или Layout-конструктору) для получения этих данных.
@Service
@BindTo(RteUpsellQuery::class)
class RteUpsellFetcher(
private val client: EatsUpsellApiClient,
) : Fetcher<RteUpsellQuery, RteUpsellResponse>() {
override suspend fun fetchQuery(query: RteUpsellQuery): () -> Response<RteUpsellResponse> {
val request = client
.eatsV1UpsellV1RteMenuRecommendationsPost(requestBody)
.build()
return { request.enqueue() }
}
}
Если нужно, на этом этапе могут применяться эксперименты и конфиги. Например, для разных групп пользователей могут использоваться разные наборы данных или разные настройки отображения.
5. Мобильный бэкенд: Rendering
Для описания визуальных моделей секций используется интерфейс RenderData
.
data class RteUpsellWidgetRenderData(
val imageUrl: String,
) : RenderData
Для их рендеринга используется DivkitSectionRenderer
. Он:
- Получает
RenderData
, дополненный данными, отSectionPlanner
. - Для каждого элемента UI (виджета) вызывает соответствующий Renderer.
- Renderer использует DivKit DSL для формирования описания UI. Он «собирает» UI из примитивов DivKit (контейнеры, текст, изображения, кнопки и т. д.), задавая им параметры (размеры, цвета, отступы, шрифты) на основе полученных данных.
- Результатом работы Renderer’ов является Snippet — фрагмент описания UI в формате DivKit.
- RenderingInterceptor объединяет все Snippet’ы в единый документ — JSON, который представляет собой полное описание UI экрана.
- Этот JSON-документ и есть конечный результат работы мобильного бэкенда.
@Service
@BindTo(RteUpsellWidgetRenderData::class)
class RteUpsellWidgetRenderer : DivkitSectionRenderer<RteUpsellWidgetRenderData, RenderingContext>() {
override fun render(
sectionId: String,
data: RteUpsellWidgetRenderData,
builder: Builder,
context: EatsRenderingContext
) {
builder.snippets += Snippet(
divan = divan {
data(
states = singleRoot(
div = image(
width = fixedSize(150),
height = wrapContentSize(),
imageUrl = url(data.imageUrl)
)
)
)
}
)
}
}
6. Ответ мобильного бэкенда
Мобильный бэкенд отправляет JSON-документ мобильному клиенту. Ответ также «упакован» в формат, понятный «Flex SDK».
{
"ui": {
"sections": [
…
],
"settings": {
"refreshable": true,
"scrollable": true,
"pagination": {
"triggerFactor": 0.3,
"minPagingThreshold": 100.0
},
"enableSnippetsAnimations": false
},
"actions": {
"onShow": {...},
"type": "section"
},
"scaffold": {
"topView": {...},
"bottomView": {...},
"backgroundColor": "#00000000",
"type": "DivkitScaffold"
},
"actions": {
"onAwake": {...},
"onFinish": {...}
},
"transitionSettings": {
"onShow": {
"duration": 0.25,
"transition": "crossDissolve"
}
}
}
7. Мобильный клиент: «Flex SDK»
- «Flex SDK» на стороне клиента получает ответ от бэкенда.
- Он распаковывает ответ, извлекая из него JSON-документ с описанием UI.
- «Flex SDK» раскладывает элементы DivKit по нужным местам на экране в соответствии с иерархией, заданной в JSON.
- Запускает процесс рендеринга DivKit.
8. Мобильный клиент: DivKit
- DivKit (клиентская часть) получает JSON-описание UI.
- Он преобразует это описание в нативные элементы Android/iOS и выводит их на экран.
- DivKit также обрабатывает события пользовательского ввода, такие как нажатия, свайпы, ввод текста и, если это необходимо, отправляет их обратно на бэкенд через цепочку «Flex SDK» — «Товарищ SDK».
В итоге, благодаря слаженной работе всех компонентов, пользователь видит на экране своего устройства динамически формируемый интерфейс, который полностью управляется с бэкенда.
Сложности при реализации BDUI
Несмотря на все преимущества BDUI, его внедрение в Еду не обошлось без сложностей. Вот с какими проблемами мы столкнулись:
- Ограниченные возможности сложных анимаций и UI-логики. BDUI отлично справляется с рендерингом интерфейсов, но когда речь заходит о сложных анимациях, нестандартных переходах между экранами и управлении визуальными состояниями, возникают трудности. Гибкость нативной разработки здесь выше, а в BDUI сложные визуальные эффекты требуют значительных усилий.
- Сложность в освоении для разработчиков. BDUI кардинально меняет привычный процесс работы. Android-разработчики быстрее адаптируются, но для iOS-специалистов переход даётся сложнее — им приходится изучать новый язык программирования и работать в непривычной среде разработки. Это замедляет внедрение BDUI и требует дополнительного времени на обучение.
- Рост сложности архитектуры приложения. BDUI требует множества библиотек и в какой-то момент приложение превращается в «слоёный пирог» из технологий. Не всегда удаётся просто и гибко настроить поведение UI или провести ручную отладку.
- Баги и различия между платформами. Несмотря на то, что ключевые библиотеки BDUI разрабатываются внутри Яндекса, периодически в них обнаруживаются баги или несоответствия в поведении между iOS и Android. Иногда на поиск бага, отправку запроса на исправление, ожидание фикса и обновление версии библиотеки может уйти больше времени, чем на разработку самой фичи.
Эти сложности не отменяют преимуществ BDUI, но заставляют нас постоянно искать способы улучшения инструментов, с которыми мы работаем.
Подводя итоги
С BDUI интерфейсы в Яндекс Еде разрабатываются быстрее, а изменения доходят до пользователей без обновлений приложения. Опытные разработчики сразу пишут код для iOS и Android, что ускоряет процесс. Правда, новичкам сначала сложнее разобраться с таким подходом, но со временем работа становится удобнее.
Мы используем единые технологии, библиотеки и API, чтобы сервисы Яндекса — Такси, Маркет, Еда — могли легко обмениваться целыми экранами. В будущем, когда BDUI охватит бóльшие части приложений, любые сервисы смогут показывать экраны друг друга без доработок. Именно к этому мы стремимся.