
Привет, Хабр! Меня зовут Никита Синявин, я ведущий Flutter-разработчик в компании BetBoom, а сегодня еще и гостевой автор Friflex. В этой статье я расскажу о своем Opus Magnum — создании эффективного Backend-Driven UI под названием Duit.
Backend Driving UI (BDUI) — это подход к разработке приложений, когда бэкэнд управляет не только данными, как это обычно бывает, но и версткой. Как любой подход, он имеет свои очевидные плюсы и минусы. Среди преимуществ BDUI я хочу выделить упрощенное A/B-тестирование. Очень приятная фича, которая помогает разработчикам на нативе, да и на Flutter тоже: максимально быстрая доставка ценностей пользователю в виде обновлений, то бишь time-to-market.
Но ложка дегтя в этом тоже есть, и среди наиболее неприятных минусов подхода я выделяю расходы на обработку сообщений сервера и вследствие этого большую зависимость приложений от скорости сети и ее состояния. Ну и просто неприятный рабочий момент в виде усложненной отладки, потому что мы, как мобильные разработчики, получаем в нагрузку еще и бэкенд. На этом небольшое лирическое отступление заканчиваю и перехожу к решению.
Предпосылки создания фреймворка
Считаю важным начать с того, откуда растут ноги Duit, потому что это весьма неожиданное место. Сначала было приложение на React Native (в BetBoom я пришел еще React Native разработчиком). Застарелые проблемы самого React Native приводили к тому, что практически невозможно было организовать качественный рефакторинг и добиться тех самых заветных 60 FPS.
И поэтому очень вовремя подоспела задача по глобальному редизайну мобильного приложения, в ходе которого мы смогли убедить бизнес в том, что Flutter — это круто, и давайте хотя бы попробуем его использовать. Долгое время ключевой фичей React Native оставался код-пуш, который, перейдя на Flutter, мы потеряли. И многие могут верно заметить, что у нас же есть shorebird — это аналог код-пуша, но только для Flutter. Но когда мы рассматривали его к применению, он был достаточно сырой, iOS был в глубокой бете, и в целом он не давал желаемого результата. А бизнес он не устраивал просто как сервис в связи с общей ситуацией.
Таким образом команда пришла к концепции Backend Driving UI. И на тот момент, когда мы рассматривали доступные решения, они либо не устраивали нас по функциональности, либо эти решения в тот момент не поддерживали Flutter вовсе. И команда вроде бы смирилась с этим и решила по старинке, в классической манере обновлять приложение через стор. Но не смирился я.
И решением стала разработка своего фреймворка Backend Driving UI — я решил это делать соло.
Требования к решению
Вот основные требования, которые я изначально хотел заложить и которые должны были обеспечивать классный пользовательский опыт — как для юзеров, так и для разработчиков.
Быстродействие. Потому что как разработчик, так и пользователь любит, когда приложение, во-первых, работает быстро, а во-вторых, интерфейс отзывчив и просто красив.
Простота интеграции. Подразумевает возможность фреймворка удобным образом встраиваться уже в существующие приложения и не вызывать проблем с настройкой в тех приложениях, которые только создаются.
Расширяемость. Это требование добавить механизмы, которые позволят разработчикам переопределять некоторые части фреймворка для своего удобства, а также расширять, допустим, какие-то виджеты и их поведение.
JSON Builder. Такая странная формулировка, но я очень хотел уйти от концепции верстки на сервере JSON, потому что глубоко убежден, что это приводит к ошибкам в виде банальных опечаток. Поэтому JSON Builder — это просто библиотеки для языков программирования Go и JavaScript, которые предоставляют нам набор функций и в будущем — продвинутые функциональности на стороне сервера для нашего фреймворка.
Эффективное обновление UI. Под этим я подразумеваю, что нужно было достичь такого процесса обновления, когда мы избегаем максимального количества мусорных перерисовок.
Обработка действий пользователя. Потому что не секрет, что пользователи постоянно с нашими предложениями взаимодействуют (статичные странички не канают).
Основные фичи
Чтобы лучше понимать, как работает Duit, важно рассмотреть фундаментальные концепции и фичи в его основе.
Контролируемые виджеты
Вокруг этой концепции строится все обновление UI. Контролируемый виджет — это виджет, состояние которого может быть обновлено извне. Если у вас возникли ассоциации со stateful и stateless виджетами Flutter, вы правы. Контролируемый виджет под капотом использует stateful, но он обновляемый.
Зачем это все? Если бы мы просто использовали stateful виджеты везде, то это бы породило кучу объектов State, что не рекомендуется: лишние накладные расходы, ухудшение перформанса. Явное указание «контролируемости» — это способ заранее оптимизировать UI.
Атрибуты
Контролируемость — это одно, но Flutter-виджеты имеют множество свойств. Для управления ими в Duit используются атрибуты — классы, которые хранят свойства виджетов и помогают в парсинге данных из JSON. Они отвечают за привязку состояния к UI и помогают виджетам понять, что именно нужно обновить.
Драйвер и контроллеры
Драйвер — центральная сущность Duit. Он отвечает за конфигурацию, обработку действий пользователя и входящих событий. Контроллер связывает драйвер и виджеты, обеспечивая двунаправленную связь и управление действиями.
Архитектура MVC
Когда все эти сущности (виджеты, данные, контроллеры) складываются вместе, мы получаем архитектуру, очень напоминающую классический MVC. Это не случайность: MVC оказался удобным для точечного обновления UI и простой в реализации.
Дальше — фичи. Их я разделяю на два больших раздела: UI-фичи и инфраструктурные фичи.
UI-фичи
Кастомные виджеты
Экосистема Flutter огромна, и включить в базу Duit все возможные виджеты из сторонних community библиотек невозможно. Поэтому был реализован API Custom Widget, который позволяет встраивать любые специфичные виджеты в пайплайн обработки Duit.
На сервере (в TypeScript) описывается конфигурация: ширина, высота, контент. Виджет наследуется от CustomWidget
, SingleChildCustomWidget
или MultiChildCustomWidget
(в зависимости от потребностей). Ключевое — это tag
, связывающий конфигурацию на сервере с реализацией на клиенте.
На клиенте кастомный виджет подключается через три шага: Attribute Factory (отвечает за парсинг JSON в атрибуты) → Model Factory (собирает модель из данных) → Build Factory (создает сам UI).
Все три связываются в регистрации кастомного виджета и становятся доступны в верстке.
Компоненты
Одна из главных болей BDUI — безумный копипаст JSON-верстки. Чтобы избежать этого ада, я вдохновился подходом DivKit и реализовал компоненты: отдельное описание макета (один tag
) + передача разных наборов данных.
Чтобы переиспользовать компонент в разных драйверах и избегать повторной загрузки шаблонов, в Duit реализована отдельная регистрация компонентов.
Макеты можно подгрузить с сервера, обработать и зарегистрировать:
final arr = await http.get("/components");
DuitRegistry.registerComponents(arr);
Это особенно хорошо, если у вас в приложении несколько независимых драйверов и вы хотите шарить компоненты между ними.
Компоненты поддерживают разделение данных и шаблона, повторное использование между драйверами, простую привязку данных через ObjectKey
и attributeKey
.
Действия и события
Каждое действие — это структура, описанная на сервере, а событие — реакция на его выполнение. UI-триггер → контроллер → драйвер → выполнение действия → событие → обновление виджета. Все четко.
Есть несколько типов действий: сетевые (HTTP/WebSocket), локальные (все внутри клиента) и скрипты (экспериментально, для кастомной логики).
Также реализована поддержка зависимостей действий: например, собрать данные из TextField
и отправить на сервер.
Анимации
Flutter поддерживает implicit и explicit анимации. В Duit реализована поддержка tween-based анимаций через описание типа tween (например, double, color); длительности, начального и конечного значения; триггера (onBuild
, onAction
) и метода (forward
, reverse
, repeat
, toggle
).
Анимации описываются декларативно и встраиваются в JSON-разметку.
Инфраструктурные фичи
Переопределение транспортного слоя
По умолчанию Duit поддерживает два сетевых протокола: WebSocket и HTTP. Но в реальных проектах часто используются более высокоуровневые клиенты, например, Dio. Поэтому была реализована возможность подмены транспорта.
Что для этого нужно:
Создать свой класс конфигурации, унаследованный от
TransportOptions
, и добавить туда свои инстансы, например, Dio.Реализовать интерфейс транспорта с методами:
execute
— для выполнения сетевых запросов,connect
— для получения начального макета с сервера.
Применить новый транспорт к драйверу через расширение extension
.
Интеграция с нативом
Хотя Duit сам по себе чисто Flutter-фреймворк, он может быть встроен в нативные приложения с помощью Flutter Add to App. Это открывает возможность использовать его как движок BDUI даже в уже существующих проектах на iOS или Android.
Скрипты и интеграции
Поддерживается возможность подключать внешние рантаймы (например, для JavaScript), что позволяет исполнять произвольную бизнес-логику на клиенте. Пока эта фича в экспериментальной стадии.
Сравниваем Duit и DivKit
Перейдем к моей любимой части.
Для тех, кто не в курсе, DivKit — это BDUI-фреймворк с открытым исходным кодом от Яндекса. Базово он поддерживает Web, Android и iOS, и с недавних пор Flutter.
Первое заметное отличие — это семантика использования. DivKit использует собственный набор виджетов и модификаторов, которые на стороне платформы интерпретируются для единообразного отображения. В Duit немного другое предназначение. Он сделан чисто под Flutter и реплицирует коллекцию виджетов Flutter как есть: виджеты и их свойства. Поэтому, когда мы используем JSON-builder на сервере, разметка для Flutter-разработчика выглядит максимально родной и понятной.
Оба фреймворка сделали шаблоны: в DivKit это templates, в Duit — компоненты. Хотя, по сути, это идейно одна и та же история.
Добавление собственных виджетов. DivKit вроде как поддерживает DivKit Extensions, которые позволяют расширять поведение базовых виджетов (надеюсь, я не прав, и туда можно добавлять свои). В свою очередь, Duit позволяет полноценно внедрять виджеты в пайплайн обработки через свою конфигурацию.
По платформам: у DivKit это iOS, Android, Web и теперь Flutter. У Duit — только Flutter. Но тут появляется интересная опция: через Flutter Add to App мы можем интегрировать его в нативные приложения и использовать как движок BDUI.
Обработка пользовательских действий. В DivKit это действия, таймеры, интерактивные элементы, где результатом действия является новый стейт. В Duit идеологически близкая система: есть действия и события, где событие — это результат выполнения действия и он описывает обновление состояния одного виджета.
Анимации. DivKit пока не поддерживает анимации во Flutter. В Duit они есть: с помощью метаописаний мы можем внедрять анимации, построенные на AnimatedBuilder и контроллерах, и это дает большую гибкость.
Выполнение скриптов. В DivKit, насколько я знаю, не поддерживается в принципе. В Duit — честно, тоже пока в очень экспериментальном формате, но есть реализация абстракций для запуска бизнес-логики.
И переходим к последнему, но в контексте open source даже первоочередному. Это документация. В DivKit — два чата на двух языках, там разработчики помогают. В Duit — документация в перманентной работе, потому что разработчик один-единственный и он не любит писать документацию (это я).
Продуктовые кейсы и метрики. У DivKit — это давно испытанный инструмент, на нем уже куча всего написано. Duit пока — полигон для разработки, но уже живой и работающий.
Зачем создавать подобные инструменты
Если честно, чаще всего BDUI — это история проприетарная. О закрытых фреймворках говорят на конференциях, но их не используют сторонние разработчики вне компании. И это приводит к тому, что мы имеем набор самых разных реализаций с самыми разными архитектурными особенностями.
И если ты — просто пользователь фреймворка, а не его разработчик, ты не переиспользуешь свои знания, которые мог бы получить на работе. А это грустно.
Теперь немного о моей мотивации — просто почему я этим вообще занимаюсь.
Open source-инструмент не гарантирует тебе, что ты сможешь решать с его помощью свои задачи. Очень часто сталкиваешься с неприятными ограничениями, какими-то компромиссами, которые тебе вообще не подходят. И поэтому я считаю, что надо развивать более широкое применение этой технологии.
И лейтмотив — альтернатива важна. Потому что мало фреймворков, которые решают задачу BDUI, а под Flutter — так вообще. Потому что конкуренция двигатель прогресса: когда фреймворков много, рубиться с ними интереснее. Потому что хочется пользоваться нормальными инструментами — качественными, отлаженными, документированными. Альтернатива важна, потому что она дает больше возможностей нам, как инженерам.
Расскажите в комментариях: если бы вы делали свой BDUI-фреймворк под Flutter, на что сделали бы упор в первую очередь?