Server-Driven UI (SDUI) — это подход для динамичного и гибкого пользовательского интерфейса, когда сервер посредством API сообщает приложению, какие компоненты и с каким контентом отображать. Он довольно популярен, и мы его тоже используем на многих экранах — помогает быстро выпускать фичи в продакшн.
В статье покажу, на каких экранах мы его применяем, и расскажу, как развивались у нас подходы гибкого UI, какие плюсы и минусы мы вывели из его использования. Сначала рассмотрим формы на динамических полях, контракты и тонкие моменты создания новых полей. Потом поговорим про динамические флоу, как за ноль калорий на стороне фронта добавлять новых провайдеров для проведения оплаты и закончим зависимыми полями. Всё это с примерами экранов мобильного приложения.
Об авторе:
Анна Саботович
В Android-разработке с 2010 года, техлид в Альфа-Банке
Динамические поля
Динамические поля у нас появились, потому что мы хотели иметь возможность настраивать состав экрана с бэкенда: когда форма меняется в зависимости от профиля пользователя и открытых на него фича-тогглов (feature toggle).
На картинке ниже вы видите примеры экранов на динамических полях. Сейчас разных полей уже больше 50, и с их помощью получается добиться большого многообразия UI.
Они работают так:
от сервера приходит массив моделей с разными типами и набором параметров;
мы парсим этот массив и маппим на компоненты дизайн-системы;
в итоге получается экран, сверстанный на компонентах, состав которого легко и быстро меняется с бэка.
Список динамо-полей можно сочетать со статической вёрсткой и вставлять его в любое место экрана.
Контракт динамических полей
Покажу пример ответа с динамическими полями.
В ответе от бэка на любой из ендпоинтов может прийти массив моделей динамических полей. Мы их различаем по полю type
и маппим в разные модели, в зависимости от типа.
Кроме типа у динамо-поля должны быть:
id
— чтобы отличать поля друг от друга;label
— чтобы отрисовать заголовок поля.
Разные типы динамо полей маппятся в разные компоненты дизайн-системы, от простых, типа строкового заголовка, до сложных, например, поле выбора счёта.
Поля делятся на две категории:
output
поля, которые просто выводят данные, напримерHEADER
илиSTRING
;input
поля — интерактивные: пользователь может поменять, например,INPUT
,CHECKBOX
,ACCOUNT_SELECT
.
У интерактивного поля может быть булев параметр required
. Если он true
, то форма не будет отправлена на бэк, пока это поле не будет заполнено. Также у интерактивных полей может присутствовать модель, которая описывает правила валидации, например минимальная или максимальная длина строки, и любые регулярки.
Примечание. Также важна обратная совместимость. Мы решили, что если фронт по какой-либо причине не может распарсить модель от сервера, то мы не отображаем поле.
Новые поля
Продакты любят динамо-поля, поэтому всё время появляются запросы на новые типы. И тут возникает две проблемы:
Первая проблема: часто при получении дизайна нового поля думаем — заводить под это поле новый тип или лучше расширить какое-то из существующих?
Здесь мы стараемся соблюсти баланс между универсальностью поля и сложностью его контракта.
Если новое поле отличается минимально, то просто добавляем в модель новые параметры.
Если оно кардинально новое, или мы видим, как сделать его универсальным сразу, то заводим новый тип и придумываем контракт для него.
На картинке показали примеры полей с разными типами, хоть они во многом и похожи. Последнее поле — самое новое. Оно в итоге объединит первые два.
Вторая проблема: дизайнеры пытаются добавить в динамо-поля слайдеры с картами и невероятной анимацией, которые будут использоваться только на одном экране. На картинке как раз пример такого динамо-поля — это кастомный прогресс бар для одной единственной фичи.
Как раз динамо-поля помогли нам решить задачу быстрой и гибкой настройки контента экрана.
Когда у бизнеса появился запрос настраивать не только состав одного экрана, но и вообще порядок следования экранов друг за другом и зависимость контента следующего экрана от результатов пользовательского ввода на предыдущем, у нас появился многошаг.
Многошаг
Это апишка, которая на вход принимает данные с текущей формы, а на выход отдает набор динамо-полей для следующего экрана.
Количество шагов многошага формально бесконечно, его конфигурация определяется на бэке. На картинке показали пример возможного многошага.
Этот многошаг состоит из трёх шагов:
Шаг №1. Пользователю приходят динамо-поля для ввода имени, даты рождения, электронной почты и дисклеймер. Также в респонсе есть модель для настройки кнопки внизу экрана.
При нажатии на неё сначала происходит валидация формы на фронте по правилам, которые зашиты в каждое динамо-поле.
Затем формируется массив с
id
полей и введенными в них данными.Этот массив отправляется в API многошага, а он отдаёт новый массив динамо-полей. Например, на втором шаге могут прийти поля для ввода паспорта, если человек старше 14 лет, или для ввода свидетельства о рождении, если младше 14.
Шаг №2. От сервера приходят данные для ввода паспорта. После ввода данных, пользователь нажимает кнопку «Продолжить» и всё повторяется: валидация, сбор значений полей в массив, отправка на бэк, получение данных для нового экрана.
Многошаг будет продолжаться пока в ответе не придёт признак, что текущий шаг последний.
Шаг №3. Последний шаг может быть просто терминальным экраном, например финальным, с информацией, что всё заполнено. А может быть экраном, ведущим на флоу подтверждения оплаты.
Вырожденный вид многошага — одношаг: вторым шагом он всегда переходит на финальный экран.
Многошаг — это уже более строгая апишка. По сути, это одна активити с ресайклером, так что рисовать на ней мы можем только динамо-поля, никакой дополнительной статической верстки быть не может.
Все платежи в пользу сторонних провайдеров в Альфа-Мобайл у нас реализованы на многошаге. При выборе провайдера, например, ЖКУ Москвы или оплаты штрафов, вызывается диплинк, который ведёт на базовый экран многошага и дергает API с Query-параметрами из диплинка.
Этот механизм даёт нам возможность за ноль калорий на стороне фронта добавлять новых провайдеров для проведения оплаты, изменять в них количество шагов, делать фиксы. Кроме того, любые флоу, которые укладываются в концепцию многошага, могут быть быстро развернуты на всех платформах с использованием уже имеющихся динамо полей. Нужно только сделать точку входа на фронте, а вся остальная логика уже будет реализована на бэке.
Многошаг отлично дополнил возможности динамо-полей, он позволил нам делать гибкие последовательности экранов. С расширением внедрения многошага и динамо-полей стали появляться кейсы изменения состояния экрана без обновления страницы или перехода на следующий шаг, например, чтобы показать или скрыть какие-то поля на форме в зависимости от выбора чекбокса.
Для решения этой задачи мы сделали поля зависимыми друг от друга.
Зависимые поля
Перед вами пример, который стал пилотом для реализации зависимых динамо-полей. Здесь стояла задача гибко настраивать подписку, например на штрафы. Если пользователь выбрал свитчер «Оплачивать счета автоматически», то мы показываем ему поле с выбором счета списания и возможностью установить лимит платежа.
Если пользователь выбрал установить лимит, то показываем ему поле ввода лимита.
Чтобы это реализовать, мы сделали надстройку над массивом динамо полей, в которой записаны правила зависимости полей друг от друга и их поведение. Эту надстройку назвали хэндлеры. Они приходят при загрузке экрана вместе со списком полей и обновление экрана происходит локально, без дополнительных запросов на бэк.
Схема работы зависимых полей
Выглядит так.
Сейчас расскажу подробнее, как это работает.
От бэка приходит массив полей и хендлеров, экран отрисовывается.
Пользователь взаимодействует с каким-либо полем, например меняет свитчер. Выбрасывается событие с id измененного поля и типом произошедшего события.
Это событие поступает на вход в библиотеку хэндлеров. Ищется подходящий этому событию триггер для хэндлера. Если он найден, значит у этого поля есть зависимые поля.
Дальше проверяются условия срабатывания хэндлера, например, свитчер включен или выключен.
Если условие выполнено, то выполняются операции заложенные в хэндлер, например скрыть определенные поля. Если никаких условий у хэндлера нет, то операция выполняется сразу.
Примечание. Каскадные выбрасывания событий у нас пока не реализованы, но в контракт такая возможность заложена.
Рассмотрим подробно каждый элемент хэндлера.
Триггер
Определяют, для каких событий должен срабатывать данный хэндлер.
Триггеры содержат параметры:
id
поля, по которому определяется, от какого поля должно прийти событие на обновление;тип события, по которому определяется, по какому типу события должен срабатывать хэндлер, например,
SELECT
,CHANGE
илиCLEAR
.
Когда какой тип триггера срабатывает:
SELECT
— когда выбрано значение из выпадающего списка;INIT
— сразу же после получения массива динамо полей;CLEAR
— при очищении поля;CHANGE
— при изменении значения в поле.
Условия
Условия — это логические выражения, которые проверяют и сравнивают данные с формы. Если они истинны, то хэндлер выполнится.
Условия могут быть составными, и количество уровней вложенности не ограничено. Они могут сравнивать, как со статическим данными, так и с данными, взятыми из любого поля или из контекста выполнения экрана. При этом условия необязательны в хэндлере: если их нет, то хэндлер выполнится в любом случае при срабатывании триггера.
Некоторые из реализованных условий:
EQUALS
— проверка на равенство;CONTAINS
— равно одному из значений из списка;NOT
— отрицание;AND
/OR
/LESS
— и/или/меньше.
Операции
Операции описывают список действий, которые должен сделать хэндлер.
Если операций несколько, то они исполнятся в том порядке, в котором указаны в JSON. Все существующие операции синхронные, но мы подумываем об асинхронных походах в сеть.
Некоторые из реализованных операций:
HIDE_FIELDS
/SHOW_FIELDS
— скрыть или показать поля из списка;SET_VALUE
— установить значение в поле;SET_REQUIRED
— сделать поле обязательным (или нет).
Итоги
Динамические поля, хэндлеры и многошаг вместе — это мощный инструмент, который покрывает большинство наших кейсов, когда нужен динамический UI.
К плюсам я бы отнесла:
Быструю реализацию новых фич на уже существующих компонентах.
Быстрый багфикс и поставка изменений клиенту.
Минусы тоже присутствуют:
Большой оверхед при разработке на старте. Приходится долго согласовывать контракты между всеми платформами и стараться всё время думать об универсальности и расширяемости.
Много логики и на бэке и на фронте, это даёт высокий порог вхождения для новых разработчиков.
Дизайнеры часто упираются в ограниченность разработанных компонентов и хэндлеров, что влечёт постоянный выбор: «ехать» на реализованных компонентах и упростить дизайн, расширять компонент или вообще делать новый.
Вы узнали о некоторых шагах развития SDUI, которые наша команда прошла за время жизни проекта. Но каждый день пользователи и бизнес ставит перед нами новые задачи, и есть уверенность, что наши подходы будут развиваться дальше и помогут решать сложные инженерные задачи. Надеюсь, наработки будут полезны и вам.
Этот пост сделан на основе недавнего доклада Анны Саботович на конференции Mobius. Если захотелось посмотреть этот доклад целиком и вообще приобщиться к конференции — 22 июня у Mobius пройдёт офлайн-день в Санкт-Петербурге. Если приобрести билет на него, то заодно получите видеозапись доклада Анны и многих других. А если хотите больше узнать о работе в Альфа-Банке — обратите внимание на Telegram-канал Alfa Digital Jobs. Там мы интересно и весело рассказываем про нашу работу, делимся новостями и полезными советами, иногда даже шутим.
Рекомендуем почитать другие статьи:
Комментарии (15)
Simipa
01.06.2022 17:26+2О, мы такое в Сберсервисе делали в 20 году, даже и не знал что это какой-то устоявшийся паттерн)
Washburne
03.06.2022 11:37Вот вроде постик неплохой, но блин, Альфа, серьезно, зачем забивать комменты дешёвыми ботами и 24*7 восторгающимися сотрудниками по поводу и без? Я не так чтобы давно читаю хабр, постоянно последние месяцев 7, но такого себе даже упоротые хостеры не позволяли, кажется. Хотя их корпблоги это отдельное. У вас опять директора с пиаром обновились, что ли, и раз не моргентшерны, то накрутки?
Вот этим мусором вы превращаете пост во что-то среднее между отзывами на алиэкспрессе и каким-то виси.
Jug.ru, вы-то как это одобрили, у вас обычно максимально техничные и вылизанные (в хорошем смысле) посты :(
Поставил свой первый минус к посту с момента регистрации на Хабре.AlfaTeam Автор
03.06.2022 11:57Здравствуйте! Приносим извинения за такой казус. Мы не просили специально забивать комментарии, пара человек хотели поддержать Анну и ее статью. Наша вина, что мы забыли предупредить, что так делать не стоит и можно поддержать другими способами. Мы бы и предупредили, если бы специально нагоняли комментарии. Мы же понимаем, что такой способ добавлять комментарии не самый эффективный, кринжовый можно сказать, и он не работает, и делали бы иначе, если бы специально действовали по такой стратегии. В данном случае, как уже говорили, специально не нагоняли комментарии, поэтому и получилась такая ситуация.
DevilDimon
Backend-driven UI для мобильного разработчика - абсолютное зло, которое превращает нативщика, учитывающего тонкие детали и отличия платформы в создателя очередного парсера.
Некоторые BDUI даже с сервера получают layout и реализуют свой декларативный велосипед для его отображения, собирая те же грабли, что html, RelativeLayout, autoresizing masks и другие достижения UI-рендеринга прошедших десятилетий.
Зло, конечно же, необходимое, но для меня скучнее работы, чем описанная в статье, придумать сложно. Завлечь разработчика такой, по-моему, будет трудно.
boronov
Не знаю насколько это зло, но для бизнеса это довольно крутая штука позволяющая быстро что-то внедрять / обновлять. Насчет разработчиков согласен, лично я бы не стал долго таким заниматься, очень скучно )
Fantasi_Anna
Как здорово, что в мире разработки найдутся задачи на любой вкус и интерес ????
Fantasi_Anna
на вкус и цвет все фломастеры разные, мне лично нравится учавствовать в разработке такого фреймворка, там и нативных штук много и сложных инженерных решений
rdevjke
Так одно дело разрабатывать фреймворк, а другое им пользоваться. Часто первое интереснее, чем второе.