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

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

Меня зовут Евгений Новиков, я создаю приложения на Flutter и сделал собственный фреймворк для fullstack-разработки на Dart.

Многие могли видеть рекламу моего Telegram-канала или даже меня самого — год назад я выступал на CrossConf в Москве.

В этой статье хочу рассказать про фреймворк и путь его создания.

С чего всё началось

Fullstack Dart появился в моей жизни 5 лет назад — я ушёл с работы, чтобы развивать собственную студию. Я предполагал, что лучше выбрать один стек как специализацию и развиваться в нём. (Сейчас я так не думаю.)

Я уже владел Flutter немного и очень верил в него. (И сейчас верю.) Поэтому меня особенно привлёк зарождавшийся тогда Serverpod — помню, что первые проекты были на версии 1.1 или 1.2. Их лозунг — missing server for Flutter, а главная фишка — кодогенерация контракта между сервером и приложением: это касается и моделей, и методов API.

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

Тем не менее я был авантюристом и выбрал свой путь. Сначала я просто делал проекты на Serverpod, но очень скоро начала проявляться моя склонность к оптимизации. Мне жутко не нравилось, что под каждый запрос от приложения нужно делать отдельный эндпоинт, обрабатывать в нём ошибки и формировать ответ.

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

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

Тогда и зародилась идея DartWay — разработать единую универсальную методологию для разработки приложений.

Но от рождения идеи до её внятной реализации прошло немало времени. Вспоминаю свои первые наброски — это была жуткая боль.

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

Выглядело это примерно так — кусок страницы из проекта тех времён:

// Модель достаётся из общего пула и ищется прямо в виджете
final CourseModel courseModel = context
    .watch<EntityListState<CourseModel>>()
    .list!
    .firstWhere((e) =>
        CourseFields.id.of(e) ==
        context.read<Map<NavigationParams<dynamic>, dynamic>>()[
            NavigationParams.id]);

// Поля — не course.title, а через енум
return AppScaffold(
  title: CourseFields.title.of(courseModel) ?? '-',
  // ...
);

Потом целыми неделями сидел над тем, чтобы склеить динамические типы, приходящие от сервера, и дженерик-методы в репозитории. Отдельным приколом стала минификация типов в продакшн-сборках для веба, об которую всё ломалось))

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

Идея и философия DartWay

Самое главное, что нужно понимать про DartWay: это ужасно opinionated фреймворк. Он предписывает буквально всё — от логики взаимодействия с бэкендом до именования переменных и структуры папок.

DartWay родился из одной простой гипотезы: для каждого вопроса можно подобрать настолько универсальное и гибкое решение, что 99% проектов смогут им пользоваться и не париться.

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

А главный вызов — как обеспечить гибкость функционала и реализацию любых запросов клиента при строгой структуре и на единых механизмах.

Что для этого главное в DartWay.

1. Строгий контракт с бэкендом

Клиент может только отправлять на сервер и получать с него модели — точнее, DTO. Все запросы проходят через один и тот же конвейер.

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

На фронте все ответы сервера тоже обрабатывает единый репозиторий и раздаёт подписчикам.

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

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

final class ListNews extends DwListRequest<NewsPost> with _$ListNews {
  const ListNews();

  @override
  List<DwLiveChannel> get channels => const [
    DwLiveChannel(DartwayExampleChannel.news),
  ];
}

final class PublishNews extends DwActionCommand<NewsPost> with _$PublishNews {
  const PublishNews({required this.title, required this.text});

  final String title;
  final String text;
}

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

DwCallHandler.command<PublishNews, NewsPost>(
  access: AppAccess.staff,
  handle: (ctx, command) async {
    final me = await ctx.profile;
    final row = await ctx.db.newsPosts.insert(
      NewsPostRow(
        authorProfileId: me.id!,
        title: command.title.trim(),
        text: command.text.trim(),
        createdAt: DateTime.now(),
      ),
    );
    final post = (await ContentObjects.news(ctx.db, [row], author: me)).single;
    ctx.publish(AppChannels.news, post);
    return post;
  },
),

В приложении — живой список и кнопка, которая не опубликует дважды:

ref.watch(dw.request(const ListNews()))   // AsyncValue<List<NewsPost>>

onTap: dw.action((_) => dw.command(PublishNews(title: title, text: text)))

Сотрудник публикует новость — она появляется у всех, у кого список открыт, на своём месте, без перезагрузки. Отказ приходит кодом и показывается на языке пользователя. Повторная команда выполняется один раз. Ни эндпоинта, ни маршрута, ни клиентского метода, ни кэша для этого никто не писал. Под капотом — POST /dw/<Type> на каждый тип и один сокет /dw/live для живых каналов.

2. Строгие правила структуры приложения

Строгие инструкции по структуре Flutter-приложения, архитектуре фич и границам ответственности. Отделение логики от UI — не рекомендация, а набор правил, которые проверяют линтер и скрипты. Циклические зависимости и бесконтрольное разрастание файлов исключены.

Так выглядит фича в шаблоне проекта:

lib/app/profile/
  profile_page/
    profile_page.dart              ← единственный публичный файл фичи
    logic/
      profile_change.dart
      profile_page_commands.dart
    widgets/
      avatar_picker.dart
      profile_settings_widget.dart
  identity/                        ← вложенная фича, те же правила
    profile_identity_section.dart
    logic/identity_change_controller.dart
    widgets/identity_change_sheet.dart

dartway check следит, чтобы так и оставалось: у фичи один публичный файл, в чужие logic/ и widgets/ ходить нельзя, циклов между фичами нет, файл длиннее 350 строк — повод разделить. Нарушение выглядит так:

lib/app/home/home_page.dart imports the internals of app/profile/profile_page
(lib/app/profile/profile_page/widgets/avatar_picker.dart) — only the public
file of a feature may be imported from outside it

Именно благодаря этим правилам я не беспокоюсь о коде, который пишут агенты. Худшее, что может случиться, — локальная неоптимальность внутри фичи на 500 строк. Структура проекта и корректность архитектуры гарантированы.

3. Готовый фундамент и модули — и при этом гибкие

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

Другая крайность — готовые пакеты. Сколько я их видел, везде одна и та же проблема: работает ровно один сценарий, поменять нельзя буквально ничего, кроме цвета кнопок. В итоге на первом же проекте приходится форкать и править под себя.

Я убеждён, что главное достоинство DartWay — баланс готового и гибкой настройки. Фреймворк закрывает все основные задачи: аутентификацию, репозиторий, веб-сокеты, работу сервера. Но везде есть колбэки и параметры, чтобы решить любую проектную задачу.

Например, вход. Аккаунты и способы входа — забота фреймворка: он знает, что кто-то вошёл и с каким телефоном или почтой. А кто этот человек для проекта — профиль, роль, что делать с его данными при удалении — решает проект в колбэках:

DwAuthConfig(
  // как доставить код — SMS, почта, свой шлюз
  deliverCode: (ctx, kind, identifier, code, accountId) async { ... },

  // профиль создаётся вместе с аккаунтом, в той же транзакции
  onAccountCreated: (ctx, accountId, kind, identifier, origin) async {
    final profile = await ProfileChanges.create(ctx, accountId, origin);
    ctx.publish(AppChannels.admin, await ProfileObjects.profile(ctx, profile));
  },

  // удалить данные человека или оставить «надгробие» — решает проект
  onAccountDeleting: (ctx, accountId) async { ... },
)

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

И отдельный уровень — шаблон. Многие вещи не зашиты в пакеты фреймворка, а сделаны в шаблоне проекта: они готовы к использованию, но никак не ограничивают редактирование и настройку.

Из последнего — сделал симпатичный дашборд в админке. Теперь такой же будет из коробки в каждом новом проекте.

Дашборд «Статистика» в админке
Дашборд «Статистика» в админке

Именно этой гибкости мне не хватало в Serverpod — и это привело к решению от него отказаться.

Почему я отказался от Serverpod

Больше трёх лет мы с командой делали проекты на Serverpod, и первая версия DartWay базировалась на нём — по сути, DartWay был модулем для Serverpod.

На старте это было удобно — взять готовую основу. И Serverpod был очень неплох в этой роли: достойная реализация сервера, ORM и генерации контракта, очень качественная документация и отсутствие багов. Как фреймворк для проекта я могу рекомендовать его и сейчас. Но как фундамент для DartWay он стал меня ограничивать.

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

Архитектура модулей

Serverpod даёт готовые модули аутентификации и чатов. Но все модели в них лежат внутри модуля — и проект не может добавить новое поле в класс пользователя или свой тип сообщения в чат.

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

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

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

Результат — 100% гибкость.

Контракт моделей и API

Главная фишка Serverpod — генерация зеркальных моделей для бэка и фронта. Зеркальных, потому что это всё-таки разные классы, хоть и называются одинаково. Не знаю, почему они пошли этим путём, но у подхода два неприятных эффекта.

Нельзя шарить бизнес-логику. Раз это разные классы без общего предка, нельзя просто навесить extension и сделать единую валидацию на фронте и на бэке. Насколько я знаю, в последнем релизе для этого появилось какое-то решение, но детально я уже не смотрел.

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

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

В Serverpod для этого есть костыли — превратить модель в её публичную версию или закрыть отдельные поля как server only. Но каждый раз это было индивидуальное решение, а я хотел единый, универсальный подход.

Поэтому в новом DartWay модели живут только на сервере. Контракт между приложением и сервером вынесен в shared-пакет и состоит из DTO — отдельных сущностей под каждый сценарий взаимодействия приложения с сервером.

Почему так долго

Отказ от Serverpod назревал давно. Я понимал, что он ограничивает меня и создаёт трудности, но пугала необходимость переписать такие сложные модули, как HTTP-сервер, ORM и генерация контракта. С развитием нейросетей всё поменялось…

Новый DartWay — на агентах и для агентов

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

Сейчас я твёрдо убеждён, что единственный реальный процесс будущего — разработка в режиме Dark Factory, когда агенты ведут разработку полностью самостоятельно, включая код-ревью и приёмочное тестирование.

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

Неделя без Serverpod

Так закончилась и история с Serverpod. 13 сентября я на импульсе попросил Claude подготовить план, около часа мы обсуждали детали — и через день новая версия DartWay была готова: свой сервер прямо на dart:io и своя ORM поверх postgres. Ещё несколько дней ушло на перенос проектов, и 19 сентября Serverpod не осталось нигде.

Самое удивительное — всё заработало сразу. Не помню ни одного бага, связанного именно с переписыванием.

Конституция для агентов

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

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

И безопасность по умолчанию: без правила доступа обработчик не запустится. Каждая подписка требует вошедшего пользователя и правила для своего канала; канал, для которого правило не написали, отказывает всем.

Поэтому у меня нет плавающих багов. Данные, сервер, авторизация — всё с одного запроса. Даже вход через Google и Apple заработал с первого раза. Это заслуга не нейросети, а методологии, внутри которой она работает.

Из 658 коммитов во фреймворке 458 написаны в соавторстве с Claude — и код в проектах на нём тоже пишут агенты. Для них в DartWay есть три вещи:

  • Тулкит — 22 навыка для агента: как добавить фичу, написать миграцию, протестировать. Недавно ужали его втрое: агенту тоже вредно читать лишнее.

  • Закон синхронности — API, пример, шаблон, документация и заметка о миграции меняются в одном PR. По этим заметкам агент сам обновляет проекты.

  • Журнал решений — каждое архитектурное решение записано с причиной. Агент читает, почему сделано так, а не угадывает.

Заключение

Что дальше

Фреймворк пока 0.x, и я этого не скрываю. Релизы выходят в ветке stable, а проект переходит на новую версию одной командой dartway update — по тем самым заметкам о миграции. На нём работают наши живые продукты.

Ближайший фокус — автотесты: их количество и точность. И сам процесс разработки: меньше циклов код-ревью и меньше токенов на задачу.

Меня волнует не только код пакетов, но и весь процесс создания приложения. Поэтому вокруг фреймворка я строю DartWay Studio — готовую фабрику разработки на AI-агентах. Так же, как с фреймворком, я прорабатываю все процессы продуктовой разработки, чтобы сделать создание приложения простым и прозрачным. Сейчас Studio в закрытой бете.

Попробовать фреймворк

Откройте пустую папку в любом AI-ассистенте — Claude Code, Cursor, Codex — и попросите выполнить две команды:

dart pub global activate dartway_cli
dartway quickstart

Дальше скажите, что хотите: «создай проект DartWay my_app и подними его». Получится работающее приложение — вход по одноразовому коду, профили, роли, админка, навигация, свой UI-кит, тесты с обеих сторон — и ни одной доменной модели: домен ваш.

Если попробуете и что-то сломается — issue на GitHub или сообщение мне помогут больше, чем звёздочка. Репозиторий: github.com/dartway/dartway, документация — dartway.dev.

А чтобы следить, как развиваются DartWay и Studio, — подписывайтесь на мой Telegram-канал @ai_native_by_novikov.

Источник: https://habr.com/ru/articles/1098410/


Интересные статьи

Интересные статьи

На первый взгляд миграция из Kaiten в Bitrix24 выглядит как обычная интеграционная задача: прочитать данные из одного REST API и записать в другой REST API.Но это впечатление быстро проходит, когда на...
Привет, Хабр! Принес вам кейс о том, как мы с командой оптимизировали работу «Навигатора мер поддержки» на портале ГИСП с помощью Python/Flask. Напомню: я по-прежнему Алексей Постригайло, более 20 лет...
Ежемесячная аудитория мессенджера Telegram в России в конце IV квартала 2023 года достигла показателя более 85 млн человек, свидетельствует анализ обезличенных данных Big Data от&n...
Проведение оценки персонала не только помогает определить уровень компетенций и профессиональных навыков сотрудников, но и выявляет потенциал для их дальнейшего развития. Однако, несмотря на её значим...
Как обновить ядро 1С-Битрикс без единой секунды простоя и с гарантией работоспособности платформы? Если вы не можете закрыть сайт на техобслуживание, и не хотите экстренно разворачивать сайт из бэкапа...