Итак, здравствуйте! Меня зовут Никита Синявин. Я руководитель направления мобильной разработки в компании BetBoom, автор телеграм-блога Boltotogy Tech и BDUI-фреймворка для Flutter — Duit. Также являюсь лидером сообщества мобильных разработчиков Mobile Assembly | Калининград.

Ранее я уже публиковал статью, которая было посвящена первому "дню рождения" фреймворка. И как бы мне не было сложно в это поверить, но со старта работы над проектом прошло уже два года. Тем интереснее будет остановиться, оглянуться назад и посмотреть на то, что удалось сделать и чего удалось достичь.

Если кратко — за год проект "перерос сам себя". Фреймворк стал более архитектурно чистым, гибким и устойчивым. Он стал более стройным и простым на первый взгляд и на порядок более сложным "под капотом".

В обиход разработки вошло много новых подходов: выработаны принципы тестирования проекта и обеспечения его качества, опробованы в "боевых" условиях AI-инструменты для кодинга и "разбавления одиночества" (я так называю AI-ревьюеры). А также свой вклад в проект внесло два человека!

Это был длинный, тяжелый, но невероятно интересный год. Давайте посмотрим на него вместе!

От целей к результатам

Прошлая статья закончилась важным этапом — постановкой целей и декларацией идей. И что особенно приятно — цели были выполнены.

Полноценная документация, нацеленная на вовлечение комьюнити и снижение порога входа. Работа с контрибьюторами и появление первых pull request’ов. Существенное расширение библиотеки виджетов — с 38 до 84! Все это — лишь видимая часть "айсберга".

Под поверхностью происходили не менее значимые изменения:

  • Была принята система версионирования и ведения проекта, добавлены автоматические проверки и CI/CD.

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

Но, пожалуй, самое интересное — это то, как идеи прошлого года начали обретать форму.

  • Идея так называемой «Джсоносжималки» (программа для "уплотнения" json с целью уменьшения объема передаваемых данных) была забракована и трансформировалась в разработку библиотеки chunk_norris — реализацию подхода Progressive JSON в Dart, что позволит гибко управлять загрузкой чанков данных.

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

  • А идея визуального UI-редактора не просто оформилась в полноценное видение инструмента и первые прототипы, но и стала катализатором работы над новым мажорным обновлением, которое меняет самые основы Duit.

Этот год можно описать коротко: Duit перестал быть просто библиотекой и встал на путь становления экосистемой. Вокруг него выстраивается собственная культура разработки, свои инструменты, подходы и люди. И именно это — главный результат прошедшего года.

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

Обновленная архитектура ядра

Пойдем "от сохи". Атрибуты — специальные классы, реплицирующие наборы свойств виджетов Flutter и отвечающие за:

  • Преобразование данных из JSON в типы Dart/Flutter.

  • Хранение и предоставление доступа к этим данным в рантайме.

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

Однако со временем выяснилось, что под этой красотой скрываются серьезные издержки — архитектура перестала масштабироваться:

  • Беспощадный бойлерплейт и “ручной труд”.
    Каждый класс атрибутов требовал скрупулёзного описания: перечислить все свойства, реализовать парсинг из JSON, прописать методы обновления и копирования. Даже самый простой класс занимал сотню строк кода и создавал риск мелких, трудноуловимых ошибок.

  • Избыточные аллокации и давление на GC.
    Механизм обновления выглядел так: приходит новое состояние → создается новый экземпляр атрибутов → создается третий экземпляр, который объединяет изменения старого и нового экземпляра. В сценариях анимаций, где обновления происходят на каждом кадре, этот процесс порождал тысячи временных объектов. Как следствие — частые срабатывания GC, микролаги и ощутимая потеря производительности.

Между циклами сборки мусора "жило" очень много объектов в ходе анимаций
Между циклами сборки мусора "жило" очень много объектов в ходе анимаций

Эти проблемы стали системными и требовали радикального пересмотра архитектуры. Продолжать "латать" модель атрибутов было бессмысленно — настало время полностью ее переосмыслить!

В Duit v4 атрибуты уступили место новой модели данных, построенной вокруг Map<String, dynamic> и механизма extension types. Такое решение может показаться шагом назад — ведь мы теряем строгую типизацию.

Но на деле всё наоборот:

  • Вместо тяжелого копирования объектов применяется слияние по ключам

  • Строгая типизация обеспечивается за счет extension types

  • Сохранение объектно-ориентированного способа взаимодействия с данными на уровне виджетов

Сердцем обновленного ядра стал DuitDataSource — надстройка над Map<String, dynamic>, реализованная с помощью extension types. Он выполняет роль строго типизированного слоя доступа к данным, скрывая под собой всю сложность парсинга и преобразований.

Теперь вместо множества разрозненных классов атрибутов фреймворк работает с единым универсальным источником данных, обеспечивающим:

Типобезопасность

DuitDataSource предоставляет набор строго типизированных геттеров и методов, возвращающих значения в нужных формата — ColorEdgeInsetsAlignment и т.д.

  @preferInline
  TextAlign? textAlign({
    String key = FlutterPropertyKeys.textAlign,
    TextAlign defaultValue = TextAlign.start,
    Object? target,
    bool warmUp = false,
  }) {
    // Читаем текущее значение свойства из Map
    final value = _readProp(key, target, warmUp);

    // Если значение имеет ожидаемый тип -> сразу возвращаем его
    if (value is TextAlign) return value;

    // Если значение = null -> сразу возвращаем defaultValue
    if (value == null) return defaultValue;

    // Значение не удовлетворяет условиям быстрого выхода из функции.
    // Требуется преобразование.
    switch (value) {
      // На основе типа значения из Map пытаемся выполнить сопоставление
      case String():
        // envAttributeWarmUpEnabled - константа
        if (envAttributeWarmUpEnabled) {
          if (warmUp) {
            return _textAlignStringLookupTable[value];
          } else {
            // Перезаписываем преобразованное значение по ключу
            // только в том случае, если оно было преобразовано.
            return _json[key] = _textAlignStringLookupTable[value];
          }
        } else {
          return _json[key] = _textAlignStringLookupTable[value];
        }
      case int():
        if (envAttributeWarmUpEnabled) {
          if (warmUp) {
            return _textAlignIntLookupTable[value];
          } else {
            return _json[key] = _textAlignIntLookupTable[value];
          }
        } else {
          return _json[key] = _textAlignIntLookupTable[value];
        }
      default:
        return defaultValue;
    }
  }

Гибкость

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

Предсказуемость

Обновления применяются через слияние, а не копирование, что исключает побочные эффекты и “залипание” старых данных.

final attr1 = <Strging, dynamic>{
  "value1": 1,
  "value2": 2,
};

final update = <Strging, dynamic>{
  "value1": 100,
  "value3": 3,
};

void main() {
  attr1.addAll(update)

  // {
  //   "value1": 100, значение было обновлено и сохраняется между обновлениями
  //   "value2": 2, значение осталось без изменения
  //   "value3": 3, новое значение из обновления
  // };
}

Под капотом фреймворк активно использует механизмы Dart VM — инлайнинг, константные lookup-таблицы и фичи компилятора Dart. Это позволило добиться впечатляющих результатов:

  • в 15 раз быстрее стала подготовка обновлений по сравнению с моделью на атрибутах

  • в 4,5 раза (в среднем) уменьшилось время выполнения по сравнению с методами парсинга из v3 ( 0.372 us → 0.077 us).

  • нулевая аллокация временных объектов в критическом пути обновления

Благодаря DuitDataSource, фреймворк получил новый уровень эффективности — теперь система обработки данных работает на пределе возможностей Dart, сохраняя при этом чистоту архитектуры и удобство для разработчиков.

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

Что нового?

Обновлённая объектная модель

В ранних версиях Duit между данными и виджетами находился слой объектной модели — классы, которые связывали атрибуты и конкретные Flutter-виджеты. Это был логичный и элегантный шаг на старте, но каждый новый (в том числе и кастомный) виджет требовал написания и применения этой прослойки.

"Если у нас все json, то почему не применить подход с extension types и тут?" - подумал я. В Duit v4 я частично отказался от классов в пользу extension types, создавая лишь один экземпляр модели для рутового виджета.

Результат — обработка JSON ускорилась в три раза, а API стало заметно проще. Теперь создание нового виджета не требует дополнительного бойлерплейта, а создание и регистрация кастомных виджетов значительно упростилась — один виджет = одна функция. А приятным бонусом послужило дополнительное снижение количества аллокаций для создания элементов объектной модели.

Мутабельные макеты компонентов и JSON Patch

В старой реализации компонентов (метода шаблонизации) существовала коварная проблема — “липкие данные”. Когда одно свойство оставалось неизменным между обновлениями, оно как бы “прилипало” к старому состоянию из-за того, что мутабельные объекты хранились по ссылке. Решение пришло в виде частичной реализации применения патчей согласно спецификации JSON Patch (RFC 6902).

Теперь каждое обновление состояния представляет собой набор декларативных операций (addreplaceremove), применяемых поверх текущего дерева данных. Это сразу дало понятный эффект — обновления стали атомарными и безопасными — никакой мутабельности и побочных эффектов.

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

Remote command API — новая степень интерактивности

До этого момента любые действия в интерфейсе проходили только через изменение состояния. Такой подход подходил для большинства задач, но плохо работал в сценариях, где требовалась реактивность — показать диалог, запустить анимацию карточки, открыть BottomSheet. Теперь в фреймворке появился Remote Command API — отдельный канал взаимодействия, не требующий обновления состояния и не зависящий от атрибутов.

Команды позволяют выполнять действия поверх текущего UI:

  • запускать анимации,

  • открывать и закрывать диалоги,

  • показывать уведомления или временные оверлеи.

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

Fragment — шаг к модульности и эффективности

Другим важным элементом обновления стали Фрагменты (Fragments) — неизменяемые и не зависящие от внешних данных части интерфейса, которые можно кэшировать и переиспользовать между экранами. Ранее каждый экран загружался как единое дерево JSON, даже если 80% интерфейса совпадали. Теперь же фрагменты позволяют определять общие блоки (например, навбар, футер и тд) и подставлять в верстку лишь указание использовать закешированный фрагмент.

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

Они разные
Они разные

Эффект оказался следующим:

  • Снижение нагрузки на сеть и клиент. JSON стал меньше, обновления — быстрее.

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

Фрагменты — это еще один шаг к рациональному описанию UI, что позволяет снизить влияние роста размера JSON на приложение.

Эволюция серверной инфраструктуры

Свой собственный этап трансформации прошел пакет duit_go - DSL для Go
Его кодовая база была переработана практически с нуля:

  • Обновлена минимальная версия Go

  • Объединены пакеты и унифицированы модули

  • Убран generics-слой, создававший лишнюю сложность

  • Добавлена кодогенерация для форвардинга методов встроенных структур

А главным нововведением стала реализация паттерна fluent builder, который превратил декларацию UI из громоздких конструкторов в цепочку читаемых вызовов.

// Вместо мешанины инициализаторов...
func main() {
	widget := SliverSafeArea(
		&duit_attributes.SliverSafeAreaAttributes[duit_edge_insets.EdgeInsetsAll]{
			Left:  true,
			Top:   true,
		},
		"id",
		false,
		nil, //child
	)
}

// получаем стройный и понятный код
func main() {
	widget := duit_widget.SliverSafeArea(
		duit_attributes.NewSliverSafeAreaAttributes().
			SetLeft(true).
			SetTop(true),
		"id",
		false,
		nil, //child
	)
}

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

Вместо заключения

Когда я только начинал работу над Duit, мне казалось, что это будет просто эксперимент. Маленькая либа, попытка упаковать UI в JSON и сделать из этого что-то живое. Сегодня это уже нечто большее — проект, у которого есть свой уникальный вектор развития и ниша.

Фреймворк "учится" быть быстрым, эффективным и надёжным. Теперь предстоит сделать его еще и удобным. Развитие идей визуального редактора, в котором можно составлять UI из блоков, перемещать их, настраивать, связывать между собой позволит сделать из Duit удобную платформу для запуска быстрых экспериментов и проверки гипотез.

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

Два года назад это была идея. Сегодня — сообщество, контрибьюторы, пользователи. И именно это, наверное, главное достижение. Когда твой код становится не просто инструментом, а точкой притяжения.

И я искренне верю, что это только начало.

Полезные ссылки:

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


  1. itatarchenkoru
    16.11.2025 08:08

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

    Можно ли провести аналогию с SSR в web разработке, с разницей в том, что компоненты встроены в приложения. Фреймворк имеет инструменты для отправки с сервера описания компонентов, а на клиенте парсер и builder, который создает из библиотеки компонентов необходимы интерфейс?

    Есть ли у Вас boilerplate для начала работы с фреймворком?