惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
A
About on SuperTechFans
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Blog — PlanetScale
Blog — PlanetScale
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
The GitHub Blog
The GitHub Blog
Vercel News
Vercel News
G
Google Developers Blog
J
Java Code Geeks
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
Cloudbric
Cloudbric
L
LINUX DO - 最新话题
MyScale Blog
MyScale Blog
H
Heimdal Security Blog
PCI Perspectives
PCI Perspectives
Attack and Defense Labs
Attack and Defense Labs
S
Security @ Cisco Blogs
Latest news
Latest news
I
Intezer
L
Lohrmann on Cybersecurity
C
CXSECURITY Database RSS Feed - CXSecurity.com
月光博客
月光博客
T
Threatpost
博客园 - 【当耐特】
S
Schneier on Security
P
Privacy International News Feed
G
GRAHAM CLULEY
T
Tenable Blog
AWS News Blog
AWS News Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
雷峰网
雷峰网
博客园 - Franky
Engineering at Meta
Engineering at Meta
美团技术团队
S
Secure Thoughts
T
Troy Hunt's Blog
Microsoft Security Blog
Microsoft Security Blog
SecWiki News
SecWiki News
V
Visual Studio Blog
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cisco Talos Blog
Cisco Talos Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Martin Fowler
Martin Fowler
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
H
Hackread – Cybersecurity News, Data Breaches, AI and More

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Опенсорсим yx_navigation — декларативную навигацию для Flutter
Андрей Смирнов · 2026-05-13 · via Все публикации подряд на Хабре

Навигация во Flutter — это постоянные компромиссы. Сначала кажется всё просто: push и pop. А потом проект растёт, появляются табы, вложенные модули, диплинки — и выясняется, что каждый следующий экран открывается по‑разному, а pop() в одном месте ведёт себя не так, как в другом. 

Navigator 1.0 прост и понятен, но при масштабировании рассыпается. Navigator 2.0 даёт полный контроль, но требует столько бойлерплейта, что проще изобрести свой фреймворк. Сообщество это поняло — и появились пакеты поверх Navigator 2.0. go_router упрощает жизнь, но недавно перешёл в режим поддержки: только баг‑фиксы, никаких новых фич. auto_route даёт type‑safety, но тянет за собой кодогенерацию.

Мы прошли через все эти варианты в процессе разработки Яндекс Про — приложения для водителей и курьеров, где навигация включает сотни фич, несколько команд, вложенные модули, табы, диплинки и legacy‑код на Navigator 1.0. А ещё — сложную логику переходов, где точный контроль над состоянием навигации не просто желателен, а критичен: экран закрывается там, где не должен, стек оказывается в неожиданном состоянии, и разобраться в причинах через стандартный API почти невозможно.

Первым кандидатом стал go_router — на то были веские причины: пакет разрабатывался Flutter‑командой, хорошо поддерживался, имел большое сообщество и де‑факто считался рекомендуемым выбором для Navigator 2.0. Казалось разумным обернуть его в удобный API и жить спокойно. Но в процессе обнаружили, что go_router принципиально не умеет обновлять соседний стек навигации в неактивной вкладке — для приложения с табами и фоновыми событиями это оказалось блокером. Типовой сценарий разберём ниже. И ни одно из существующих решений не закрывало наши потребности полностью.

Так появился yx_navigation — новый пакет в нашей экосистеме архитектурных решений для Flutter, после yx_scope (DI) и yx_state (управление состоянием). Дальше расскажу, с какими трудностями мы столкнулись, какие требования сформулировали, как устроен yx_navigation и как именно он решает проблемы крупных приложений.


Предыстория: навигация в Яндекс Про

Яндекс Про — это платформа выполнения заказов. Водители, курьеры и другие исполнители — каждое направление со своими фичами, экранами и сценариями. Разработкой занимается несколько команд, каждая отвечает за свой модуль. И каждая когда‑то выбирала свой способ навигации.

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

  • Разнобой подходов. Одна команда работала с go_router, другая — напрямую с Navigator 1.0, третья экспериментировала с Navigator 2.0. Единого подхода не было, и при стыковке модулей возникали «интересные» баги.

  • Непрозрачность состояния навигации. В произвольный момент было невозможно понять, что сейчас вообще открыто, какие экраны в стеке. Синхронизировать бизнес‑логику с состоянием навигации было крайне затруднительно — интерактор мог запросить переход, не зная, в каком контексте находится приложение.

  • Несколько способов открыть один и тот же экран. Разные вызовы давали разное поведение: анимация, свайп, стиль перехода. Разработчик должен был помнить, какой способ «правильный» для конкретного контекста. Material в одном месте, Cupertino в другом — и да пребудет с вами знание, где, что и как.

  • Проблемы с pop() и выходом из фичи. Закрыть экран и вернуться назад — казалось бы, всё тривиально. Но при вложенных навигаторах, модальных окнах и табах логика pop() становилась непредсказуемой: выход из фичи мог сломать стек родительского навигатора или повлиять на соседнюю вкладку. «Почему после закрытия диалога закрылся соседний экран?» — классический вопрос при разборе багов.

Что не так с существующими решениями

Мы детально изучили существующие пакеты навигации — go_router, auto_route, beamer, routemaster, octopus и другие — и даже попытались использовать go_router как основу с обёрткой поверх. Вот что получилось:

  • go_router перешёл в maintenance mode: только баг‑фиксы, никакого развития. Для нас это неприемлемо — мы вкладываемся в архитектуру на годы вперёд. Помимо этого, go_router не поддерживает изоляцию фич: все маршруты живут в едином дереве, и любая часть приложения может навигироваться куда угодно. Навигация привязана к BuildContext — из бизнес‑логики управлять ею нельзя.

  • auto_route — зрелый пакет, но завязан на кодогенерацию. В крупном проекте build_runner — это боль: десятки секунд на каждый запуск, конфликты сгенерированных файлов при мёржах, дополнительный шаг в CI/CD. И те же проблемы: нет изоляции фич, нет управления навигацией из бизнес‑логики без BuildContext.

  • beamer и routemaster — интересные альтернативы, но ни одна не решает ключевую для нас задачу: дать нескольким командам возможность работать над изолированными фичами с собственной навигацией, вкладывать их друг в друга, иметь единое реактивное состояние и не зависеть от UI‑контекста.

  • Octopus заслуживает отдельного упоминания: декларативный роутер с деревом состояния, вложенной навигацией и guards — по духу нам близок, пакет сделан грамотно. Но на pub.dev он до сих пор версии 0.0.9, то есть без гарантий стабильности API для продукта нашего масштаба. И главное: он не закрывает весь набор наших требований — ту же изоляцию фич между командами приходится добивать снаружи.

Требования к нашему решению yx_navigation

На основе обсуждений в рабочей группе мы собрали список требований — то, без чего навигация в нашем масштабе нежизнеспособна:

  • Универсальность. Решение должно одинаково работать в отдельной фиче и в корневом приложении.

  • Модульность. Навигационный модуль одной фичи можно вложить в другой — и получить общее дерево навигации.

  • Изолированность. Модуль знает только о своих маршрутах и не может влиять на состояние навигации родителя или соседей.

  • Реактивность. В любой момент можно получить текущее состояние навигации, подписаться на его изменения и управлять навигацией через мутации состояния.

  • Business Logic First. Управлять навигацией можно из бизнес‑логики, без зависимости от BuildContext.

  • Guards. Контроль мутаций состояния: проверка авторизации, прав доступа, валидация данных перед переходом.

  • Поддержка диплинков. Связь URI и навигации: восстановление состояния из ссылки и произвольная обработка входящих URI, в том числе без смены экрана.

  • Поддержка диалогов и оверлеев. Bottom sheet'ы, диалоги и оверлеи управляются аналогично стеку навигации.

  • Поддержка табов. Управление табами и nested‑навигация в пределах табов или PageView.

  • Совместимость с Navigator 1.0. Legacy‑код с Navigator.push() и showDialog() продолжает работать без переписывания.

Архитектура yx_navigation

Как и yx_scope с yx_state, пакет yx_navigation разделён на две части:

  • yx_navigation — чистый Dart без зависимостей на Flutter (то, что принято называть flutter agnostic). Маршруты, состояние, мутации, guards, сериализация — всё, что можно покрыть unit‑тестами без запуска приложения.

  • yx_navigation_flutter — Flutter‑обвязка: виджеты, Router delegate, page factories, debug‑инструменты и прочее.

Такое разделение продиктовано практической необходимостью. Бизнес‑логика получает событие (например, «пришёл заказ») и должна обновить состояние навигации. Ей не нужен BuildContext, ей нужен доступ к дереву маршрутов. Чистый Dart даёт всё необходимое и избавляет от зависимости от Flutter.

Состояние навигации — это дерево

Центральная идея yx_navigation проста: состояние навигации — это дерево. Не стек (как у большинства пакетов), не плоский location, а именно дерево узлов RouteNode.

Текущее состояние навигации в определённый момент как дерево узлов

Текущее состояние навигации в определённый момент как дерево узлов

Каждый узел содержит:

  • route — идентификатор маршрута (YxRoute);

  • arguments — сериализуемые параметры (например, {'orderId': '123'});

  • extra — несериализуемые объекты (полезно на период миграции);

  • children — список дочерних узлов.

Каждый раз, когда мы используем привычное нам API navigator.push(route, arguments), мы создаём новый узел RouteNode или выполняем мутацию текущего узла:

Операция push(route) создаёт новый узел RouteNode в дереве состояния

Операция push(route) создаёт новый узел RouteNode в дереве состояния

Список дочерних узлов children интерпретируется по‑разному — в зависимости от типа родительского маршрута. Это может быть стек навигации: дети строятся один поверх другого как обычные страницы. Или табы: дети отображаются как вкладки в IndexedStack. Или вложенные навигаторы: каждый ребёнок представляет собой изолированную ветвь со своим собственным стеком.

Например, состояние «авторизованный пользователь, открыта главная страница с вкладками сообщений, картами и активным профилем» может выглядеть так:

Root
└── Main (IndexedStack: tabs)
    ├── Messages
    ├── Map
    └── Profile (активный таб)
Дочерние узлы: рассматриваем дочерние узлы как табы в indexed stack

Дочерние узлы: рассматриваем дочерние узлы как табы в indexed stack

Другой пример. Имеем состояние «авторизованный пользователь, открыта главная страница, за ней страницы сообщений, карт и профиля»:

Root
└── Main (Outlet: pages)
    ├── Messages
    ├── Map
    └── Profile (активный экран)
Дочерние узлы: рассматриваем дочерние узлы как последовательность экранов в стеке навигации

Дочерние узлы: рассматриваем дочерние узлы как последовательность экранов в стеке навигации

В коде это дерево состояния RouteNode:

const YxRoute(id: 'main').toNode(
  children: [
    const YxRoute(id: 'messages').toNode(),
    const YxRoute(id: 'map').toNode(),
    const YxRoute(id: 'profile').toNode(),
  ],
)

Как мы видим, такое состояние одинаково и для отображения стека экранов, и для реализации табов. Отличается только интерпретация дочерних узлов (нод) для маршрута main

За управление состоянием (RouteNode) отвечает RouteNodeStateManager. Состояние реактивно: RouteNodeStateManager предоставляет stream и state для чтения и подписки на изменения.

Сразу к практике

Давайте посмотрим на примере. Минимальный путь от нуля до работающей навигации — четыре шага.

Шаг 1. Определяем маршруты

abstract class ProfileRoutes {
  static const home = YxRoute(id: 'profile-home');
  static const driverProfile = YxRoute(id: 'profile-driver');
  static const tripsHistory = YxRoute(id: 'profile-trips-history');
  static const statistics = YxRoute(id: 'profile-statistics');
  static const settings = YxRoute(id: 'profile-settings');
  static const documents = YxRoute(id: 'profile-documents');
}

Шаг 2. Связываем маршрут с UI через декларацию

RouteDeclaration связывает маршрут с виджетом:

final driverProfileDeclaration = RouteDeclaration.routeBuilder(
  route: ProfileRoutes.driverProfile,
  routeBuilder: RouteBuilder.widget(
    builder: (context, state) => const DriverProfilePage(),
  ),
);

Тут мы определили, что если в дереве состояния появится узел с маршрутом ProfileRoutes.driverProfile, то для него будет построен виджет DriverProfilePage.

Подробности о декларациях будут ниже. Но смысл понятен: связываем состояние навигации с конкретным UI‑представлением.

Шаг 3. Собираем схему навигации

RouterSchema — это то, что группирует декларации нашего приложения или фичи и определяет начальное состояние:

class ProfileNavigationSchema extends RouterSchema {
  ProfileNavigationSchema()
      : super(
          //  здесь получаем корень дерева и задаём начальное состояние
          //  в данном случае будет единственный дочерний узел ProfileRoutes.home
          initialNodeBuilder: (node) => node
            ..setChildren([
                ProfileRoutes.home.toNode(),
            ]),
        );

  @override
  List<RouteDeclaration> get declarations => [
        //  тут определим декларацию для маршрута ProfileRoutes.home
        RouteDeclaration.routeBuilder(
          route: ProfileRoutes.home,
          routeBuilder: RouteBuilder.widget(
            builder: (context, state) => const ProfileHomePage(),
          ),
        ),
        driverProfileDeclaration, //  декларация профиля и другие
        tripsHistoryDeclaration,
        statisticsDeclaration,
        settingsDeclaration,
        documentsDeclaration,
      ];
}

Шаг 4. Запускаем

После запуска увидим первым экран ProfileHomePage.

class _ProfileAppState extends State<ProfileApp> {
  late YxRouterConfig config;

  @override
  void initState() {
    super.initState();
    final profileSchema = ProfileNavigationSchema();
    config = profileSchema.build(); //  строим RouterConfig
  }

  @override
  Widget build(BuildContext context) => MaterialApp.router(
        routerConfig: config,
      );

  @override
  void dispose() {
    config.dispose(); //  не забываем очистить ресурсы
    super.dispose();
  }
}

profileSchema.build() создаёт RouterConfig со всеми необходимыми компонентами Navigator 2.0: RouterDelegate, RouteInformationParser, RouteInformationProvider, BackButtonDispatcher.

Если хотим открыть другой экран, нам потребуется RouteNavigator. Получить его мы можем из контекста. Дальше используем знакомые по Navigator 1.0 операции push, pop и прочие (мы называем их «примитивы»):

final routeNavigator = YxNavigation.navigatorOf(context);

// Перейти на экран
routeNavigator.push(ProfileRoutes.driverProfile);

// Вернуться назад
routeNavigator.pop();

// Проверить, можно ли вернуться
final canPop = routeNavigator.canPop();

Навигатор и RouteNodeStateManager

YxNavigation.navigatorOf(context) отдаёт RouteNavigator — узкий контракт с операциями в духе Navigator 1.0 (push, pop,...). За ним стоит тот же объект, который отвечает за состояние, — RouteNodeStateManager.

RouteNodeStateManager— он реализует важный контракт NavigationController, который совмещает:

  • чтение текущего состояния RouteNode и его обновления (state/stream из RouteNodeReadable); 

  • мутации (mutate из RouteMutator); 

  • все операции‑примитивы, такие как push и pop (RouteNavigator)

  • функции ActiveRouteController, которые отвечают за активный маршрут, — полезно для работы с табами.

Для работы внутри виджета обычно достаточно типа RouteNavigator; для бизнес‑логики чаще держат ссылку на RouteNodeStateManager (или NavigationController), потому что это полный доступ к состоянию навигации без BuildContext.

RouteNodeStateManager и NavigationController реализуют следующие контракты: RouteNavigator, RouteMutator, RouteNodeReadable, ActiveRouteController

RouteNodeStateManager и NavigationController реализуют следующие контракты: RouteNavigator, RouteMutator, RouteNodeReadable, ActiveRouteController

Ключевая идея: все операции навигации — это мутации дерева состояния. Операции push, pop и другие примитивы реализованы через mutate — метод из RouteMutator, который принимает текущее состояние RouteNode и возвращает новое:

// Так реализован push — добавляем узел к children текущей ноды
void push(YxRoute route, {Map<String, String>? arguments}) =>
    mutate((routeNode) {
      routeNode.add(RouteNode.fromRoute(
        route: route,
        arguments: arguments ?? const {},
      ));
      return routeNode;
    });

Добавили узел в дерево — экран появился. Убрали — исчез. Нет возможности «открыть экран» в обход изменения дерева.

Такой подход открывает возможности, недоступные в классическом Navigator: например, можно изменить состояние ветки навигации, которая сейчас не отображается на экране. Подробнее об этом — в разделе «Мутация состояния „неактивных“ ветвей навигации».

Business Logic First: подход к работе с навигацией

Одна из ключевых особенностей yx_navigation — навигация из бизнес‑логики без привязки к Flutter и BuildContext. Звучит очевидно, но на практике большинство пакетов навигации требуют context.go() или context.pushNamed(), то есть UI‑контекст. А интерактор, сервис или обработчик push‑уведомления — не виджеты. У них изначально нет BuildContext, и это нормально для слоя бизнес‑логики. Нужен способ обновить навигацию из этого слоя без поиска любого подходящего контекста в дереве или необходимости искать GlobalKey<NavigatorState>.

Итак, что нужно сделать?

Создаём RouteNodeStateManager в интеракторе:


class ProfileNavigationInteractor {
  late final RouteNodeStateManager _stateManager;

  RouteNavigator get navigator => _stateManager;

  ProfileNavigationInteractor() {
    //  Здесь указываем корневую ноду (root) в дереве состояния. В нашем примере — ProfileRoutes.root
    _stateManager = RouteNodeStateManager(
      routeNode: ProfileRoutes.root.toNode(),
    );
  }

  /// Открыть страницу профиля
  void openDriverProfile() => navigator.push(ProfileRoutes.driverProfile);

  /// Открыть страницу настроек
  void openSettings() => navigator.push(ProfileRoutes.settings);
}

В чём тут основное отличие?

Flutter‑first‑подход: при вызове schema.build() без stateManagerConfiguration пакет сам создаёт внутренний RouteNodeStateManager с начальным деревом из схемы; RouteNavigator появляется уже в UI — через YxNavigation.navigatorOf(context) после MaterialApp.router.

Business‑logic‑first: меняется не набор API, а момент появления менеджера — тот же RouteNodeStateManager мы создаём заранее, до работы presentation‑слоя (в интеракторе, из DI), и передаём его в build. Навигатор для бизнес‑логики — это по‑прежнему контракт RouteNavigator, просто берётся из того же экземпляра RouteNodeStateManager, без BuildContext. Интерактор остаётся чистым Dart‑классом.

Получение RouterConfig выглядит теперь так:

final stateManager = coreScope.stateManager;  //  coreScope — ссылка на Scope, но это может быть любой провайдер зависимости из DI в UI
final profileSchema = ProfileNavigationSchema();
config = profileSchema.build(
  stateManagerConfiguration: StateManagerConfiguration(
    stateManager: stateManager,
  ),
);

Guards — контроль мутаций состояния

Любая мутация состояния проходит через цепочку guards. Идея знакома по auto_route и go_router: перед переходом проверяем авторизацию, права, валидацию, да всё что угодно. Guard получает текущее состояние (origin) и целевое (target) и решает: разрешить переход (next), отменить (cancel) или подменить target‑состояние (redirect).

Цепочка: мутация состояния проходит через guards до фиксации нового дерева

Цепочка: мутация состояния проходит через guards до фиксации нового дерева

abstract interface class RouteNodeGuard {
  GuardResult call(
    RouteNode origin,
    RouteNode target,
    GuardContext context,
  );
}

Пример. Проверка авторизации:

class AuthGuard implements RouteNodeGuard {
  final AuthService authService;

  const AuthGuard(this.authService);

  @override
  GuardResult call(
    RouteNode origin,
    RouteNode target,
    GuardContext context,
  ) {
    if (isInLoginNode(target)) {
      return const GuardResult.next();
    }

    if (!authService.isAuthorized()) {
      return GuardResult.redirect(
        target: AppRoutes.login.toNode(),
      );
    }

    return const GuardResult.next();
  }
}

Другой пример. Автоматическая инициализация табов:

class TabInitGuard implements RouteNodeGuard {
  final YxRoute tabRoute;
  final YxRoute childRoute;

  const TabInitGuard({
    required this.tabRoute,
    required this.childRoute,
  });

  @override
  GuardResult call(
    RouteNode origin,
    RouteNode target,
    GuardContext context,
  ) {
    final mutableTarget = target.toMutable();
    final tabNode = mutableTarget.findByRoute(tabRoute);

    if (tabNode != null && tabNode.children.isEmpty) {
      //  Как только нода найдена, автоматически добавляем в неё детей
      tabNode.setChildren([childRoute.toNode()]);
      return GuardResult.redirect(target: mutableTarget);
    }

    return const GuardResult.next();
  }
}

Guards указываются в декларациях или на уровне схемы RouterSchema:

final someDeclaration = RouteDeclaration.routeBuilder(
  route: AppRoutes.protectedPage,
  guards: const [
    AuthGuard(),
    PermissionGuard(),
    DataValidationGuard(),
  ],
  routeBuilder: /* ... */,
)

Guards выполняются последовательно. Если любой вернёт cancel() — последующие не выполняются. При redirect() проверка перезапускается с новым target. next передаёт выполнение следующему Guard.

Схема навигации (RouterSchema)

RouterSchema — это собранный в одном месте каркас навигации как для приложения в целом, так и для отдельной фичи. Геттер declarations задаёт, какие декларации объявлены и как декларации вложены друг в друга (вложенность деклараций сейчас имеет смысл только для RouteStrictDeclaration). 

class RouterSchema {
 @internal
 final Iterable<RouteDeclaration> declarations;


 @internal
 final Iterable<RouteNodeGuard> guards;


 @internal
 final InitialRouteNodeBuilder initialNodeBuilder;
...

Колбэк initialNodeBuilder описывает начальное дерево RouteNode при старте приложения или фичи: какие дочерние узлы будут построены в корне дерева и как устроены вложенные уровни. Чтобы получить RouterConfig и передать его в MaterialApp.router, вызывают метод build() — минимальный пример с ProfileNavigationSchema есть выше, в разделе «Сразу к практике».

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

Иерархия деклараций внутри схемы для приложения/примера могла бы выглядеть так:

Схема навигации описывает иерархию деклараций и вложенность фич

Схема навигации описывает иерархию деклараций и вложенность фич

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

Типы деклараций

yx_navigation предоставляет несколько типов деклараций для разных сценариев. Базовый контракт — abstract interface class RouteDeclaration, в приложении обычно вызывают именованные фабрики:

Фабрика

Класс реализации

RouteDeclaration.

routeBuilder

RouteBuilderDeclaration

RouteDeclaration.

scheme

RouteSchemaDeclaration

RouteDeclaration.

indexedStack

RouteIndexedDeclaration

RouteDeclaration.

strict

RouteStrictDeclaration (наследует RouteBuilderDeclaration; строгий режим / переход к дочернему маршруту возможен, только если он заявлен в declarations). 

На схеме ниже не показываю

Схематично можно выразить так:

Три основных типа деклараций: RouteBuilderDeclaration, RouteIndexedStackDeclaration и RouteSchemaDeclaration

Три основных типа деклараций: RouteBuilderDeclaration, RouteIndexedStackDeclaration и RouteSchemaDeclaration

RouteDeclaration.routeBuilder — универсальная декларация (RouteBuilderDeclaration)

RouteDeclaration — это связующее звено между деревом состояния и UI. 

Напомню: дерево RouteNode — это чистые данные, в них нет ничего о Flutter. Декларация отвечает на вопрос: когда в дереве появляется узел с маршрутом X — что именно показать пользователю?

Декларация связывает YxRoute с построением UI через routeBuilder

Декларация связывает YxRoute с построением UI через routeBuilder

Вы сами решаете, как интерпретировать ноду для вашего маршрута — есть три варианта routeBuilder:

Декларация связывает YxRoute с построением UI через routeBuilder. Доступно три варианта routeBuilder

Декларация связывает YxRoute с построением UI через routeBuilder. Доступно три варианта routeBuilder

widget — простая страница. Возвращаете любой виджет:

RouteDeclaration.routeBuilder(
  route: AppRoutes.profile,
  routeBuilder: RouteBuilder.widget(
    builder: (context, routeNode) => ProfilePage(),
  ),
)

outlet — вложенный навигатор (стек страниц внутри экрана). Коллекция children рассматривается как стек навигации:

RouteDeclaration.routeBuilder(
  route: AppRoutes.home,
  routeBuilder: RouteBuilder.outlet(
    outletBuilder: (context, routeNode, outlet) {
      return Scaffold(
        appBar: AppBar(title: Text('Home')),
        body: outlet,
      );
    },
  ),
  declarations: [dashboardDeclaration, settingsDeclaration],
)

indexed — IndexedStack для реализации табов:

RouteDeclaration.routeBuilder(
  route: AppRoutes.home,
  routeBuilder: RouteBuilder.indexed(
    indexedBuilder: (context, routeNode, indexedStack, controller) {
      return Scaffold(
        body: indexedStack,
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: tabs.indexOf(controller.activeRoute),
          onTap: (index) => controller.setActiveRoute(tabs[index]),
          items: [/* ... */],
        ),
      );
    },
  ),
  declarations: [mapTabDeclaration, messagesTabDeclaration],
)

RouteDeclaration.indexedStack — лёгкий способ построить табы (RouteIndexedDeclaration)

Специализированная декларация для табов. Работать с табами можно и при помощи RouteBuilder.indexed, как в примере выше. Но, в отличие от RouteBuilder.indexed, RouteIndexedDeclaration будет рассматривать только вложенные декларации для создания табов. А также автоматически создаёт guards для управления children на основе указанных дочерних деклараций:

inal homeDeclaration = RouteDeclaration.indexedStack(
  route: AppRoutes.home,
  routeBuilder: RouteIndexedStackBuilder(
    indexedBuilder: (context, routeNode, indexedStack, controller) {
      return Scaffold(
        body: indexedStack,
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: tabs.indexOf(controller.activeRoute ?? AppRoutes.map),
          onTap: (index) => controller.setActiveRoute(tabs[index]),
          items: [/* ... */],
        ),
      );
    },
  ),
  //  Все дочерние декларации будут рассматриваться как табы
  declarations: [
    mapTabDeclaration,
    messagesTabDeclaration,
    profileTabDeclaration,
    settingsTabDeclaration,
  ],
);

При переходе на AppRoutes.home дочерние ноды для всех табов создаются автоматически через guards.

RouteDeclaration.scheme — изоляция фич (RouteSchemaDeclaration)

Подключает готовую RouterSchema как изолированный модуль. Автоматически создаёт вложенный навигатор и NavigationController для управления своим поддеревом состояния RouteNode. Подробности дальше.

Модульность и изоляция фич

В предыдущем разделе перечислены типы деклараций, в том числе RouteDeclaration.scheme для подключения изолированной RouterSchema. Рассмотрим типичную продуктовую задачу: фича как модуль в приложении‑хосте, без протаскивания внутренних маршрутов наружу.

Классическая ситуация в крупном проекте: команда A пишет фичу «Профиль», команда B — основное приложение‑хост. Фича должна подключаться одной строкой, не сообщать хосту о своих внутренних маршрутах и ничего не знать о родителе. Полная изоляция.

Ни один из рассмотренных нами пакетов этого не даёт: в go_router и auto_route все маршруты живут в одном дереве, и фича не может скрыть своё внутреннее устройство.

В yx_navigation это возможно через RouteDeclaration.scheme:

final profileSchemaDeclaration = RouteDeclaration.scheme(
  route: DriverRoutes.profile,
  schema: ProfileNavigationSchema(),
);

С точки зрения хоста фича — это просто маршрут DriverRoutes.profile. Чтобы перейти в неё, хосту достаточно вызвать:

routeNavigator.push(DriverRoutes.profile);

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

Важно, что RouteDeclaration.scheme можно использовать не только в хосте, но и внутри другой фичи. Любая RouterSchema может вложить в себя другую схему — механизм одинаков на любом уровне вложенности. Это позволяет строить произвольно глубокие иерархии изолированных модулей:

// Фича «Заказы» включает в себя фичу «Карточка заказа» как вложенный модуль
final orderCardDeclaration = RouteDeclaration.scheme(
  route: OrderRoutes.orderCard,
  schema: OrderCardNavigationSchema(),
);


base class OrdersNavigationSchema extends RouterSchema {
  OrdersNavigationSchema() : super(/* ... */);


  @override
  List<RouteDeclaration> get declarations => [
    ordersListDeclaration,
    orderCardDeclaration, // вложенная фича
  ];
}


// Дальше фича «Заказы» сама переиспользуется, например, на уровне хоста
final ordersDeclaration = RouteDeclaration.scheme(
  route: ApplicationRoutes.orders,
  schema: OrdersNavigationSchema(),
);
Важно: RouteDeclaration.scheme можно использовать не только в хосте, но и внутри другой фичи. Любая RouterSchema может вложить в себя другую схему — механизм одинаков на всех уровнях 

Важно: RouteDeclaration.scheme можно использовать не только в хосте, но и внутри другой фичи. Любая RouterSchema может вложить в себя другую схему — механизм одинаков на всех уровнях 

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

  • Собственный навигатор. У фичи свой вложенный Navigator, управляемый через изолированный NavigationController.

  • Изолированное состояние. Фича видит и мутирует только свою ветвь дерева состояния. Повлиять на состояние родителя или соседних фич невозможно.

  • Standalone‑ и Embedded‑режим. Одна и та же фича может работать самостоятельно (например, в example‑приложении) или быть встроенной в родительское приложение. Переключение между режимами — через фабрику зависимостей.

У фичи свой вложенный Navigator, управляемый через изолированный NavigationController. Фича видит и мутирует только свою ветвь дерева состояния. Повлиять на состояние родителя или соседних фич невозможно

У фичи свой вложенный Navigator, управляемый через изолированный NavigationController. Фича видит и мутирует только свою ветвь дерева состояния. Повлиять на состояние родителя или соседних фич невозможно

Свой контроллер навигации NavigationController снаружи не обязателен: если в scheme не передавать NavigationController, пакет создаст его сам — вложенный навигатор будет привязан к ветке маршрута из декларации.

Business‑logic‑first (BLF) — сценарий, когда нужен заранее созданный экземпляр (тот же, что у интерактора или в DI). Родитель собирает NavigationController.node и подключает его к фиче — либо полем navigationController: в scheme, либо через outletBuilder, если нужна обёртка зависимостей вокруг outlet.

Как это работает изнутри (явный контроллер для BLF)

Родительское приложение может заранее создать NavigationController.node — контроллер, следящий за конкретной веткой общего дерева:

final profileNavigationController = NavigationController.node(
  stateManager: stateManager,
  nodeResolver: RouteNodeResolver.id(route: DriverRoutes.profile),
);

Передать его в фичу можно напрямую в декларации:

final profileSchemaDeclaration = RouteDeclaration.scheme(
  route: DriverRoutes.profile,
  schema: ProfileNavigationSchema(),
  navigationController: profileNavigationController,
);
У фичи свой изолированный NavigationController. Фича видит и мутирует только свою ветвь дерева состояния. Повлиять на состояние родителя или соседних фич невозможно

У фичи свой изолированный NavigationController. Фича видит и мутирует только свою ветвь дерева состояния. Повлиять на состояние родителя или соседних фич невозможно

Или через outletBuilder, если кроме контроллера нужно обернуть вложенный навигатор в скоуп зависимостей:

final profileSchemaDeclaration = RouteDeclaration.scheme(
  route: DriverRoutes.profile,
  schema: ProfileNavigationSchema(),
  outletBuilder: (context, routeNode, outlet) {
    final appDependencies = DriverAppDependenciesScope.of(context);
    return ProfileFeatureDependenciesScope.embedded(
      navigationController: appDependencies.profileNavigationController,
      child: outlet,
    );
  },
);

В результате получаем:

  • Основное приложение не импортирует внутренние зависимости фичи.

  • Фича не знает о существовании родительского приложения.

  • Обе стороны работают через абстракцию NavigationController.

  • Фича может быть подключена в любое другое приложение без изменений.

Это решает реальную проблему крупных проектов: команда A разрабатывает фичу «Профиль», команда B — основное приложение. Они интегрируются через одну точку — RouteDeclaration.scheme.

Мутация состояния «неактивных» ветвей навигации

Это та самая фича, ради которой мы в итоге и написали свой пакет.

Представьте: водитель листает сообщения, а в это время прилетает заказ. Заказ должен появиться во вкладке «Заказы» в виде шторки — когда пользователь переключится на неё, он увидит накопившиеся заказы. В go_router и auto_route это невозможно: они работают только с активной ветвью навигации.

Пользователь находится во вкладке Сообщения. В табе заказов показываем шторку заказа, но остаёмся в Сообщениях. При переключении в таб заказов видим шторку заказа

Пользователь находится во вкладке Сообщения. В табе заказов показываем шторку заказа, но остаёмся в Сообщениях. При переключении в таб заказов видим шторку заказа

Простейший пример на go_router (та же тема — водительское приложение с табами, StatefulShellRoute):

// Приходит заказ — хотим добавить его во вкладку «Заказы» БЕЗ переключения.
// С go_router единственный способ — go(), который ПЕРЕКЛЮЧАЕТ вкладку, делая её активной:
_router.go('/orders/order/${order.id}?pickup=...&dropoff=...&price=...');
// Пользователя прервут и переключат на «Заказы». push() в неактивную ветвь невозможен.

У нас RouteNodeStateManager хранит полное дерево навигации, а NavigationController.node() позволяет изолировать любой узел, включая ветви, которые сейчас не видны. Типичный сценарий:

// Контроллер, привязанный к неактивной вкладке «Заказы»
final ordersController = NavigationController.node(
  stateManager: stateManager,
  //  Тут указываем, за какой частью дерева «следим»
  nodeResolver: RouteNodeResolver.id(route: AppRoutes.ordersTab),
);

// Push в неактивную ветвь — штатная операция
ordersController.push(
  AppRoutes.orderCard,
  arguments: {'orderId': 'ORD-001', 'price': '500'},
);

Этот код выполняется из чистого Dart‑интерактора без BuildContext. При переключении на вкладку «Заказы» пользователь видит актуальный стек со всеми накопившимися заказами.

Отдельно о табах: у go_router, auto_route и большинства похожих пакетов нет доступа к навигационному состоянию на том же низком уровне, что в yx_navigation — единое состояние и явный mutate. Снаружи остаются исторически выросшие из Navigator 1.0 API (push, pop, понятие «текущего» стека), которые при переносе на Navigator 2.0, StatefulShellRoute и несколько вложенных Navigator часто сохраняют те же проблемы и corner cases. Из близких по логике альтернатив о мутации всего дерева состояния заявляет разве что Octopus. У нас смена навигации — это прежде всего изменение дерева RouteNode, а не обходной путь через navigatorKey чужого, неактивного стека.

У go_router с StatefulShellRoute вызов go() на URI вложенного маршрута в другой ветви (как в примере выше) по модели пакета всегда делает эту вкладку активной. URL однозначно задаёт видимую ветвь, «докинуть» экраны во вкладку «Заказы», оставаясь в «Сообщениях», декларативно нельзя. Если пытаться менять стек неактивной ветви императивно через navigatorKey и, например, pop у соответствующего Navigator, легко поймать краш (assert в match.dart) — воспроизводимый кейс с перебором веток: flutter/flutter#132906.

У auto_route сценарий «Находясь на одной вкладке, открыть маршрут из дерева другой» разобран в Milad‑Akarie/auto_route_library#1966: context.pushRoute(BookRoute(...)) с Home на экран из вкладки Books даёт Failed to navigate to BookRoute; в том же треде видно, что pushNamed/navigateNamed либо теряют историю, либо ведут «назад» не туда, то есть без переключения активной вкладки и с ожидаемым стеком задачу не закрыть.

Поддержка кастомных анимаций и немного о Page Factory

При определении декларации маршрута мы можем указать свою кастомную реализацию класса Page через PageFactory. PageFactory определяет, как маршрут превращается во Flutter Page:

// Material (по умолчанию)
RouteBuilder.widget(
  builder: (context, routeNode) => MyPage(),
  pageFactory: const PagesFactory.material(),
)

// Cupertino
RouteBuilder.widget(
  builder: (context, routeNode) => SettingsPage(),
  pageFactory: const PagesFactory.cupertino(),
)
// Кастомная анимация
RouteBuilder.widget(
  builder: (context, routeNode) => MyPage(),
  pageFactory: PagesFactory.custom(
    builder: (context, routeNode, key, child) {
      return CustomTransitionPage(
        key: key,
        child: child,
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          return FadeTransition(opacity: animation, child: child);
        },
      );
    },
  ),
)
// Модальный диалог
RouteBuilder.widget(
  builder: (context, routeNode) => ConfirmDialog(),
  pageFactory: PagesFactory.custom(
    builder: (context, routeNode, key, child) {
      return DialogPage(key: key, child: child);
    },
  ),
)

Один и тот же билдер страницы можно использовать с разными PageFactory: на одном экране Material‑анимация, на другом — Cupertino, на третьем — кастомная.

Сериализация и десериализация состояния

Вся суть навигационного состояния — дерево RouteNode — хранится в URL. Это обеспечивает deep links, шеринг ссылок, работу браузерной кнопки «Назад» и восстановление состояния при перезагрузке. Встроенные сериализаторы формируют URI, совместимые с RFC 3986 (Uniform Resource Identifier: Generic Syntax): зарезервированные символы JSON заменяются на допустимые в URI эквиваленты, path‑сегменты и fragment соответствуют спецификации. 

За преобразование дерева RouteNode ↔ URI отвечает PlatformStateSerialization, который задаётся через RouterConfiguration:

config = schema.build(
  routerConfiguration: RouterConfiguration(
    serialization: const PrettyUriStateSerialization(),
  ),
);

Поддерживаются два встроенных типа сериализации:

  • PrettyUriStateSerialization (используется по умолчанию) — читаемый формат, удобен для отладки. Сегменты пути отражают иерархию: / — переход к ребёнку, . / .. / ... — уровень вложенности. У каждого узла свои параметры: после идентификатора маршрута пишут $?key=value (несколько пар — через &). Пример URL, где аргументы заданы на трёх уровнях:

#/driver-home$?tab=orders/.driver-documents$?folder=work/..document-detail$?documentId=dl-789

Дерево: корень driver-home (аргумент tab=orders) → children: [driver-documents (folder=work)] → children: [document-detail (documentId=dl-789)].

  • UriStringStateSerialization — компактный формат для длинных путей. Дерево кодируется в base64-строку, что даёт короткий URL при большом количестве узлов. Пример:

#('cnQ':('aWQ':'cm9vdA'),'YXJncw':{})

После обратного преобразования (ключи и строки в значениях — в base64url) это JSON узла для маршрута с id: "root", args → пустой объект, детей нет. Подходит такой формат для OAuth‑редиректов и сценариев, где третьи стороны добавляют свои query‑параметры (есть опция mergeQueryParams).

Контракт сериализации минимальный:

abstract interface class PlatformStateSerialization {
  Uri convert(RouteNode node);   // дерево → URI
  RouteNode parse(Uri data);     // URI → дерево
}

Реализовав этот интерфейс, можно задать свой кастомный формат URL. Например, поддержать сериализацию в стиле RESTful‑роутинга: ресурсы в path‑сегментах (/drivers/42/documents/dl-789) и состояние через query‑параметры (?status=active&tab=security).

Совместимость с Navigator 1.0

В крупных проектах переход на новый подход не происходит за один день. «Мы всё переписываем на yx_navigation» — звучит красиво, но в реальности в Яндекс Про десятки фич на Navigator 1.0. Код с Navigator.of(context).push(MaterialPageRoute(...)), showDialog(), showModalBottomSheet() должен продолжать работать, пока мы постепенно мигрируем.

Здесь упираемся в конфликт моделей. Декларативный RouterDelegateyx_navigation поверх него) живёт в мире page‑based‑навигации: итоговый стек описывается списком Page, состояние — деревом RouteNode

Императивный Navigator 1.0 оперирует Route: часть из них страничные (MaterialPageRoute и так далее), часть — page‑less (ModalRoute у диалогов, bottom sheet и прочих): они не участвуют в том же контракте, что список Page у делегата. Смешивание «снаружи» без compatibility layer и без адаптеров приводит к рассинхрону: операции вроде pushReplacement/pushAndRemoveUntil упираются в assert«ы (A page-based route cannot be completed using imperative api, provide a new list without the corresponding Page to Navigator.pages instead) и несогласованность стека — ровно потому, что старый Navigator 1.0 API рассчитан на другой способ обработки маршрутов.»

Compatibility Layer (NavigatorCompatibilityOverrides) перехватывает вызовы Navigator 1.0 и оборачивает/адаптирует их до той же модели, что и остальное приложение: page‑less и прочие Route приводятся к page‑based‑представлению и встраиваются в общее дерево. Как side‑эффект (полезный на период миграции или отладки): всё, куда вы зашли через старый API, также отражается в едином дереве состояния навигации — те же узлы RouteNode, что и у декларативных маршрутов. Их видно в debug‑панели, на них распространяется общая картина стека, проще сопоставить legacy‑поведение с новым.

Подключение — одна строка:

NavigationConfigProvider(
  navigatorOverrides: const NavigatorCompatibilityOverrides(),
  child: MaterialApp.router(routerConfig: config),
)

После этого старый код работает без изменений:

// Всё это продолжает работать
Navigator.of(context).push(MaterialPageRoute(builder: (_) => OldScreen()));
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => NewScreen()));
showDialog(context: context, builder: (_) => MyDialog());
showModalBottomSheet(context: context, builder: (_) => MySheet());

Покрытие — ~95% стандартных Flutter route types:

  • MaterialPageRoute, CupertinoPageRoute — полная поддержка.

  • DialogRoute, CupertinoDialogRoute, RawDialogRoute — специализированные адаптеры.

  • CupertinoModalPopupRoute — специализированный адаптер.

  • ModalBottomSheetRoute — полная поддержка.

  • Любой ModalRoute — fallback через modalRouteProxy.

Единственное исключение — PopupMenuRoute (showMenu) — private class во Flutter SDK, который пока не может быть обёрнут. Он работает в native‑режиме через обычный Navigator.

CompatibilityObserver — мониторинг миграции

Для отслеживания прогресса миграции предусмотрен CompatibilityObserver:

class MigrationTrackingObserver extends CompatibilityObserver {
  final Map<String, int> _routeTypeStats = {};

  @override
  void didCreatePagelessRoute({
    required RouteNodeReadable routeNodeReadable,
    required Route<dynamic> route,
    required String routeId,
    required String routeType,
    required RouteNode routeNode,
  }) {
    //  Собрали статистику
    _routeTypeStats[routeType] = (_routeTypeStats[routeType] ?? 0) + 1;
  }

  //  Распечатали список legacy-роутов
  void printReport() {
    for (final entry in _routeTypeStats.entries) {
      debugPrint('${entry.key}: ${entry.value}');
    }
  }
}

Подключаем:

NavigatorCompatibilityOverrides(
  observer: MigrationTrackingObserver(),
)

Теперь мы видим, какие типы route ещё используют Navigator 1.0, и можем планировать миграцию.

Debug‑инструменты

«А что сейчас в стеке?» — вопрос, который каждый разработчик задавал себе при отладке навигации. yx_navigation предоставляет встроенную debug‑панель, которая визуализирует дерево состояния в реальном времени. Вместо логов и предположений — полный снапшот всего состояния: история мутаций, guards, результаты сериализации в URI.

Включить панель просто — достаточно передать DebugPanelModeNotifier при построении схемы:

config = schema.build(
  debugConfiguration: NavigationDebugConfiguration(
    debugPanelModeNotifier: DebugPanelModeNotifier(enableDebugPanel: true),
  ),
);

Панель показывает:

  • полное дерево RouteNode с аргументами/extra;

  • активный маршрут на каждом уровне вложенности (это просто последняя нода в children);

  • сериализованное представление состояния в адресной строке;

  • историю изменений при каждой мутации.

Для разработки это незаменимо: вместо отладки «а что сейчас в стеке?» вы видите полный снимок дерева навигации.

После запуска приложения с включённой debug‑панелью (enableDebugPanel) будет доступна кнопка‑оверлей, по нажатии на которую откроется панель отладки. По умолчанию показываем дерево состояния.

Кроме состояния, можно проверить сериализованное в строку состояние (то, что можно использовать в качестве URI для flutter web), историю мутаций, а также поменять режим отображения debug‑панели.

Экосистема yx_architecture 

yx_navigation — третий пакет в экосистеме, которую мы называем yx_architecture:

Задача

Пакет

Описание

DI

yx_scope

Скоупы зависимостей с жизненным циклом, compile‑safety, без кодогенерации

State Management

yx_state

Управление состоянием с очередью операций, без бойлерплейта

Навигация

yx_navigation

Декларативная навигация с реактивным состоянием и изоляцией фич

Каждый пакет — независимый инструмент: можно взять только yx_navigation и оставить go_router для части приложения или подключить yx_state для состояния внутри фичи без остальной связки.

Собранные вместе, три пакета задают единый архитектурный контур:

  • yx_scope управляет жизненным циклом зависимостей. Скоуп фичи создаётся при авторизации и удаляется при выходе — вместе со всеми зависимостями.

  • yx_state управляет состоянием внутри фичи. StateManager живёт в скоупе и предоставляет реактивный стейт с очередью операций.

  • yx_navigation управляет навигацией между фичами и внутри них. RouteNodeStateManager навигации живёт в скоупе, интеракторы используют его для программного управления переходами.

У всех пакетов есть общие свойства:

  • Чистый Dart в основе, Flutter‑обвязка отдельно. У каждого пакета есть Dart‑ядро без зависимости от Flutter. Скоупы зависимостей, операции состояния, мутации навигации — всё это тестируется unit‑тестами без запуска приложения.

  • Без кодогенерации. Никакого build_runner ни в одном из пакетов: нет лишних шагов в CI/CD и конфликтов сгенерированных файлов при мёржах.

  • Простота для базовых сценариев, гибкость для сложных. Каждый пакет минимален для старта, но масштабируется до enterprise‑задач: изоляция фич, вложенные скоупы, сложные цепочки guards и прочее.

  • Обкатка в продакшене Яндекс Про. Все три пакета работают под реальной нагрузкой в приложении с десятками команд и сотнями фич.

Заключение

yx_navigation — наше решение проблемы навигации в крупных Flutter‑приложениях. Древовидное реактивное состояние, изоляция фич, Business Logic First, guards, совместимость с Navigator 1.0, debug‑инструменты — всё это выросло из реальных болей: багов на проде, бесконечных разборов «а почему pop() тут ведёт себя иначе» и месяцев исследований того, можно ли адаптировать существующие пакеты под наши нужды.

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

Ссылки