Widlet — pet-проект про Server-Driven UI на Dart

Привет. Меня зовут Карим, я Flutter разработчик уже 7 лет и последний месяц я делаю фреймворк для server-driven UI на Dart. Репозиторий пока закрыт, но проект дошел до состояния, когда о нем можно рассказать.

Зачем еще один SDUI

Server-Driven UI решает известную проблему: бэкенд деплоится за минуты, а чтобы поправить UI в мобильном приложении - полный релизный цикл и ожидание App Store Review. SDUI это убирает: сервер описывает интерфейс, клиент рисует.

Но у всех реализаций, которые попадались мне на глаза, есть общая черта - собственный DSL. JSON-схемы, кастомные конфиги, проприетарные форматы описания виджетов. Для каждого решения приходится учить новый язык.

При этом Flutter-разработчики уже знают хороший язык описания UI. Он называется Flutter. Отсюда идея Widlet: а что если DSL не нужен?

Название - widget + applet. Маленькое приложение из виджетов, которое запускается внутри хоста.

Flutter API как протокол

Публичный API Widlet идентичен Flutter. Не "вдохновлен", не "похож" - идентичен, насколько это вообще возможно для кода, который выполняется не на клиенте.

`Scaffold`, `AppBar`, `ListView`, `ListTile` - те же виджеты. `TextStyle`, `EdgeInsets`, `Color` - те же типы, те же имена параметров. Flutter-разработчик, глядя на этот код, не должен замечать разницы.

Разница в том, где он выполняется. Этот код работает на сервере. Клиент получает данные - какие виджеты построить, с какими свойствами, в каком порядке - и рисует.

Отклонения от Flutter API допускаются в одном случае: когда концепция физически невозможна вне клиентского runtime. `AnimationController` привязан к frame scheduler - его нет на сервере. `RenderObject` - это клиентский layout. Но `Theme.of(ctx).colorScheme.primary`, `Padding(padding: EdgeInsets.all(8))`, `GestureDetector(onTap: ...)` - все это работает и на сервере, и нет причин менять API.

UI как данные

Widlet описывает интерфейс как типизированное дерево данных: тип виджета, свойства, дети, слоты, события. Никакого Flutter, HTML или CSS внутри протокола.

`Color(0xFF1976D2)` в протоколе - просто число. Flutter-хост создаст из него `Color`. Веб-хост сконвертирует в CSS `rgba()`. Гипотетический терминальный хост подберет ближайший ANSI-цвет. Widlet не знает, кто его рисует, и не должен знать.

Это дает конкретную вещь: один и тот же Widlet-код работает на разных платформах без изменений.

Три режима запуска

Один и тот же Widlet может работать тремя разными способами. Выбор режима не влияет на код видлетов.

**Серверный (WebSocket).** Widlet на сервере, клиент подключается по сети. Передается не полное дерево при каждом изменении, а инкрементальные патчи. Бинарный кодек с интернированием строк, трафик небольшой.

**WASM.** Widlet компилируется в WebAssembly, скачивается на устройство и выполняется локально в JS-песочнице. Нет сетевого лага, работает офлайн. На Android это JavaScriptSandbox (Chromium V8), на iOS - WKWebView (нужен iOS 18.2+ для WasmGC). Песочница изолирована: прямого доступа к системе нет, все взаимодействие через типизированный RPC-канал. По сути это OTA-обновления UI без App Store.

**Изолят.** Widlet в Dart-изоляте внутри того же приложения, общение через `SendPort`/`ReceivePort`. Для модульной архитектуры: независимые UI-модули, каждый со своим состоянием.

Как рисуется UI

Сейчас есть два хоста.

**Flutter Host** строит из данных настоящие Flutter-виджеты. Material 3, темизация, нативное поведение. Это основной хост.

**Jaspr Host** делает то же самое, но в DOM. Код Widlet не менялся - поменялся только рендер.

Хосты не обязаны быть идентичными. У браузера свои возможности и ограничения, у Flutter - свои. Widlet описывает *что* показать, хост решает *как*.

Навигация

Маршруты в `WidletApp` образуют направленный граф. `edges` задают допустимые переходы между экранами. Переходы поддерживают нативные анимации на хосте.

Граф можно динамически строить и дополнять, настраивать правила переходов между экранами. Guards, `PopScope`, deep links работают поверх этого.

Двусторонний канал

Widlet - не односторонний поток "сервер говорит, клиент слушает". Связь двусторонняя: Widlet может запросить у хоста viewport, открыть URL, прочитать localStorage. Хост в свою очередь прокидывает на Widlet размеры экрана, изменения темы, тапы, ввод текста, ресайз, deep links. Widlet работает не вслепую - он знает контекст, в котором отображается.

Под капотом - `RpcPeerEndpoint` из моей же библиотеки [rpc_dart](https://pub.dev/packages/rpc_dart): каждая сторона одновременно клиент и сервер. Один канал, мультиплексированные вызовы, стримы в обе стороны.

Через кастомные контракты Widlet может использовать возможности хоста: HTTP-клиент для запросов в сеть, подключение к базе данных, вызовы нативного кода через platform channels. Widlet описывает что ему нужно, хост предоставляет реализацию.

Что из этого следует

Не все проверено в продакшене, но архитектура допускает:

- Исправление UI без релиза - поправил Widlet на сервере/выпустил релиз wasm, все клиенты увидели/обновились.

- A/B тесты интерфейса - сервер отдает разные деревья разным пользователям. Никаких feature flags в клиентском коде.

- Веб-версия - тот же Widlet, подключенный к Jaspr Host вместо Flutter Host.

- Независимый деплой модулей - команды работают над отдельными Widlet-модулями, не блокируя друг друга.

- Офлайн с обновлениями - WASM-бандл работает без сети и обновляется в фоне.

Расширяемость

Разработчик не ограничен тем, что реализовано из коробки. Хост позволяет регистрировать свои рендереры для любого типа виджетов - как переопределять существующие, так и добавлять новые. Со стороны Widlet можно регистрировать кастомные RPC-контракты для общения с хостом - если стандартных (viewport, storage, http) не хватает.

По сути, единственное настоящее ограничение - то, что завязано на 60fps: покадровые анимации, жесты с continuous feedback. Все остальное - вопрос кастомного рендерера на хосте и контракта для общения с ним.

---

Если тема интересна или есть вопросы - буду рад обсудить в комментариях.

Следите за обновлениями проекта на pub.dev: https://pub.dev/publishers/widlet.dev/packages

Мои пакеты на pub.dev: https://pub.dev/publishers/dart.nogipx.dev/packages

Telegram: https://t.me/karmarov

Начать дискуссию