Привет всем, кто хочет изменять интерфейс мобильного приложения до выхода нового релиза, всем, кто хочет без лишних доработок на клиенте проводить А/B-тестирование, и всем, кто хочет забыть о срочных «новых пятничных промоакциях», которые нужны уже в понедельник. В этой статье мы поговорим об основах Backend-Driven UI: рассмотрим абстрактно, как всё работает на бэкенде и на клиенте. 

Минутка предыстории

Сейчас я разрабатываю iOS-приложения в команде мобильной разработки Ozon Tech и по совместительству преподаю на курсах Route 256 в iOS-стриме. 

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

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

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

  • Нужно писать один и тот же типовой код для Android, iOS и веба, при этом поддерживая согласованность платформ. 

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

  • Сложно проводить А/B-тесты. 

  • В коде содержится очень много бизнес-логики. 

Решить эти проблемы поможет Backend-Driven UI. 

Backend-Driven UI 

Backend-Driven UI (Backend-Driven Development или Server-Driven UI) — это концепция разработки интерфейсных приложений с экранами и переходами, формируемыми на сервере. Бэкенд не только управляет данными в приложении, но и его версткой. 

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

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

Давайте рассмотрим пример работы на клиенте

Для наглядности разберём работу инструмента на примере простого экрана: 

Мы видим два элемента: зелёный с надписью “Hello There” и бирюзовый с заголовком, подписью и кнопкой. В Backend-Driven приложении подобный экран можно получить таким JSON-ответом: 

{ 
  "backgroundColor": "ozGreen", 
  "title": "Hello There", 
},
{ 
  "title": "Title", 
	"subtitle": "Subtitle",
  "backgroundColor": "ozTurquoise",
	"button": {
  	"backgroundColor": "ozYelloy",
		"text": "Button"					
	}
} 

Меняя JSON, приходящий с бэкенда, мы будем менять элементы или их расположение на странице в зависимости от гибкости Backend-Driven подхода. 

Backend-Driven UI подходы

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

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

Подход в Ozon

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

Каждый блок — это отдельный кастомный UI-элемент, который может содержать свою логику. В Backend-Driven UI мы можем управлять наполнением страницы, меняя контент в админке, которая называется layout management tool. Подробнее про нее мы поговорим немного позднее. Вслед за изменениями шаблона страницы в админке, изменится ответ от бэкенда.

Каждый виджет разрабатывает отдельная команда бизнесового направления, эти направления называются вертикалями. Если интересно, подробнее об этом можно прочитать в статье «Масштабируем команду мобильной разработки: как мы в Ozon справились с ростом до 44 iOS, Android и QA на одном приложении». 

Вернёмся к виджетам. На клиенте мы используем Composer SDK (ничего общего с Jet Pack Compose) — это «браузер виджетов», который умеет разбивать JSON на отдельные модели, собирать из них виджеты и показывать их в том порядке, в котором они пришли с бэка. Так как Composer SDK знает обо всех виджетах приложения, мы можем отображать любой виджет в любом месте приложения. 

Как работает Backend-Driven UI на бэкенде со стороны мобильного клиента

Весь процесс разбит на несколько этапов: 

  1. При запуске приложения мы запрашиваем страницу по URL у Composer API. Composer API — это сервис на бэкенде, запрашивающий и агрегирующий ответы от бэкенд-сервисов вертикалей. 

  1. Composer API запрашивает шаблон страницы по URL у layout management tool.

Layout management tool (LMT) — система для управления шаблонами, админ-панель сайта Ozon. Она позволяет создавать шаблоны, добавлять в них виджеты, параметризовать их и публиковать на сайте.  

В ответ на запрос Composer API, LMT возвращает массив — layout-список, в котором уже определён порядок виджетов на странице. 

  1. Composer API проходит по массиву, идёт в бэкенд-сервисы — и для каждого виджета получает от них JSON с данными. 

  1. Все данные склеиваются в один JSON и отправляются единым ответом на клиент. 

Таким образом, мы делаем один запрос и получаем один ответ, при этом Composer API не парсит JSON от сервисов, а передаёт его на клиенты в том виде, в каком он получил его. Если какой-то из сервисов отдаёт невалидный JSON, то задача Composer SDK на клиенте — не обрабатывать эту часть JSON и не показывать виджет, который должен был появиться. 

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

Немного про Layout API (Layout Management Tool)

Выше мы видели, как Composer API передал URL в Layout API и в ответе получил layout-список, по которому затем обратился с запросом к нужным сервисам. 

Layout API хранит настроенный шаблон страницы по URL, поэтому на запрос Composer API возвращается нужная страница. Упрощённо шаблон страницы карточки товара выглядит так: 

В админке мы видим различные блоки, которые можно настраивать, перетаскивать, добавлять для них разные условия. Также можно добавлять и удалять сами блоки. Так мы настраиваем шаблоны страниц. Каждому блоку на клиентах соответствует конкретный виджет, умеющий парсить JSON, который Composer API получил от бэкенд-сервиса. 

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

Контракты и их роль

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

В контракте содержится следующая информация: 

  • описание опциональности и типов полей; 

  • какие поля предназначены только для конкретной платформы; 

  • какие поля стали устаревшими (помечаются как deprecated) и бэкенд больше не присылает их для новых версий виджета. 

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

Для примера возьмём упрощённый JSON и его контракт: 

{
	"activateCode": {
		"backgroundColor": "ozRed",
		"activatedData": {
			"title": "Активируйте ваш код",
			"icon": "ic_m_activateCode"
		}
	}
}
message Contract {
    string backgroundColor = 1;
    ActivatedData activatedData = 2; // required

    message ActivatedData {
        string title = 1; // required
				string icon = 2;
    }
}

Здесь мы видим много разных полей. Разберём некоторые из них: 

  • message Contract — описание модели виджета, которая придёт на клиенты;

  • ActivatedData activatedData = 2; // required — вложенная модель, помеченная как обязательная. Если эти данные не придут, то виджет не должен будет отображаться.

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

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

  • либо добавить новое поле — и тогда для старой версии виджета можно будет отправлять новый JSON, и мы не поломаем обратную совместимость; 

  • либо убрать обязательное поле — и обратная совместимость сломается. В этом случае нам нужно будет отправлять старую версию JSON для старых клиентов, а новую версию — для новых с определённой версии приложения. Управляется это указанием версии виджета, это поле приходит в JSON вместе с vertical, component и stateId. Тогда мы сможем отображать виджет для новых и старых версий клиента. 

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

"activateCode": {
	"background": "ozRed"
}

Мы решили доработать его и добавить новое поле: 

"activateCode": {
	"background": "ozRed",
	"title": "Ваш код"
}

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

Однако если мы поменяем имя поля background на backgroundColor

"activateCode": {
	"backgroundColor": "ozRed",
	"title": "Ваш код"
}

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

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

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

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

Давайте теперь рассмотрим, как всё работает на клиенте. 

Как работает Backend-Driven UI на мобильном клиенте

Мы уже знаем, что при запуске приложения или открытии страницы мы делаем запрос к Composer API и в ответе получаем JSON, в котором содержатся layout-список и данные для виджетов. На клиенте запросы страниц и их обработка происходят в модуле Composer SDK. Задачи модуля: запросить страницу, декодировать данные, преобразовать их в модели, собрать виджеты и отобразить их на странице. Рассмотрим упрощённый пример JSON, который получает клиент:

{
  "layout": [
    {
      "vertical": "csma",
      "component": "activateCode",
      "stateId": "activateCode-123456",
      "version": 1
    }
  ],
  "csma": {
    "activateCode": {
      "activateCode-123456": {
        "backgroundColor": "ozRed",
        "activatedData": {
          "title": "Активируйте ваш код",
          "icon": "ic_m_activateCode"
        }
      }
    }
  }
}

В JSON есть два элемента: “layout” и “csma”: 

  • Layout — это тот самый список виджетов, от порядка элементов в этом массиве зависит расположение виджетов на странице; 

  • CSMA — это модель виджетов, которую Composer API получил от бэкенд-сервиса. 

Именно эти элементы Composer SDK использует, чтобы отобразить и сконфигурировать виджеты. Давайте рассмотрим поля layout: 

  • Vertical — это имя вертикали, под которой зарегистрирован виджет, по этому ключу Composer SDK находит нужную часть JSON с данными виджетов; 

  • Component — это имя конкретного виджета, по нему Composer SDK находит в JSON модели этого виджета; 

  • StateId — имя, по которому достаются модели конкретных виджетов; 

  • Version — версия виджета, которая используется, чтобы отобразить нужную версию view виджета. 

Используя vertical, component и stateId Composer SDK собрать любой виджет, а из них любую страницу. 

Как работают виджеты

Виджет — это самостоятельный UI-элемент или даже модуль, который содержит кастомную логику и модель для декодинга. 

Архитектурно виджет выполнен по MVVM и состоит из нескольких файлов: 

  • Assembly — регистрируется в конфигурации Composer SDK по ключу виджета, чтобы потом Сomposer мог вызвать Assembly для декодинга, конфигурации модели и view виджета; 

  • WidgetModel — декодируемая-модель виджета; 

  • WidgetView — кастомный view виджета; 

  • WidgetViewModel — ViewModel виджета, в которой содержится вся бизнес-логика; 

  • WidgetViewState — «состояние» виджета, с которым мы работаем. 

Вместе все эти файлы составляют отдельный модуль виджета. Assembly каждого модуля зарегистрированы в Composer SDK — и в нужный момент Composer SDK вызывает методы декодинга или сборки view у Assembly виджетов. 

Когда нам нужно разработать новый виджет, мы создаём новый модуль по шаблону, описываем модель из контракта в WidgetModel, затем верстаем UI виджета по дизайну в WidgetView и прописываем логику в WidgetViewModel. 

Если еще раз, глобально, когда Composer SDK получил JSON, по vertical, component и stateId из layout-списка, он обращается к зарегистрированной по ключу Assembly и вызывает метод сборки модели виджета (WidgetModel), передавая в него JSON с данными виджета. Assembly должна распарсить JSON и вернуть готовую модель виджета. А после, когда виджет должен показаться на экране, Composer SDK вызывает у Assembly метод сборки WidgetView и полученную view крепит на ячейку UICollectionViewCell. 

Composer SDK

Глобально, Composer — это вся Backend-Driven UI система, которая включает как Composer API на бэкенде, так и Composer SDK на iOS, Android и веб, так и админку LMT (Layout management tool). 

Composer SDK на мобильном клиенте, например на iOS — это обертка над UICollectionView с различными дополнительными сервисами.  

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

  • ComposerViewController c UICollectionView — отвечает за конфигурацию виджетов моделями и за их отображение; 

  • ComposerService — отвечает за запросы к сети и за парсинг JSON на части, которые передаются в Assembly виджетов. 

Если переводить минимальную версию на архитектуру VIPER или MVVM, то частей модуля получится больше. 

Давайте ещё раз пройдёмся по этапам работы модуля от запроса к Composer API до отображения виджетов на экране: 

  1. Запуск приложения, ComposerViewController инициализирует запрос страницы через ComposerService и получает JSON с бэкенда. 

  2. ComposerService разбивает полученный JSON на части по vertical, component и stateId. 

  3. ComposerService достаёт нужную Assembly виджета (по vertical и component) и передаёт в неё JSON для парсинга (по stateId). 

  4. Assembly декодирует модель виджета и возвращает её в ComposerService. 

  5. ComposerService возвращает все полученные модели в ComposerViewController. 

  6. ComposerViewController собирает view виджета и конфигурирует её моделью виджета. 

  7. Виджет отображается на экране. 

Если кратко, то это весь процесс работы на клиенте от момента запуска приложения до отображения виджета. Если тема будет интересна, то в следующей статье расскажу, как собрать минимальную версию Composer SDK у себя на клиенте, чтобы запустить Backend-Driven UI. 

Преимущества подхода

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

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

  • Можно моментально отключать любые виджеты или фичи. 

  • Любой виджет можно отобразить на любой странице. 

  • Можно переиспользовать виджеты для сборки новых страниц. 

Особенности, трудности и недостатки

1. Разработка виджетов

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

2. Переиспользование виджетов

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

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

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

4. Избыточность данных

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

Стоимость товара одинаковая, но, так как это отдельные виджеты, мы должны прислать эту цену три раза: в виджет цены, на красную и синюю кнопки. 

5. Связанные виджеты

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

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

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

Что в итоге

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

В этой статье мы абстрактно разобрали, как работает Backend-Driven UI на сервере и клиенте. Если хочется разобраться что делать, чтобы на iOS собрать работающий инструмент, то в следующей статье шаг за шагом разберём сборку минимальной версии ComposerSDK. 

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

Кстати, совсем скоро у нас будет открытый Mobile Meetup, где обсудим перформанс-мониторинг по-взрослому. Анонс выложим здесь и в Телеграме — приходите в гости! 

А если вы хотите попасть на бесплатный курс «Продвинутая iOS-разработка: SwiftUI и Backend-Driven UI», оставляйте заявку, стартуем в мае.   

Комментарии (1)


  1. Deterok
    30.04.2022 08:17

    Мы еще не видим что за пожар у того парня за кустами.