Dart и Flutter для десктопных приложений с xray-core

Практический курс по созданию десктопного приложения на Flutter с интеграцией xray-core. От основ Dart и Null Safety до сборки .exe через GitHub Actions. Минимум теории — максимум рабочего кода.

1. Основы Dart и Null Safety для десктопных приложений

Основы Dart и Null Safety для десктопных приложений

Почему Dart — это именно тот язык, на котором стоит строить десктопное приложение с xray-core, а не, скажем, Python или Electron? Ответ кроется в сочетании компилируемой производительности, строгой типизации и экосистемы Flutter, которая из одного кода собирает приложения под Windows, macOS и Linux. Если вы пришли из C#, Java или Go — Dart покажется знакомым, но с рядом приятных сюрпризов. Разберём их на практике.

Типизация и переменные: var, final, const

Dart — язык со строгой статической типизацией, но с выводом типов. Компилятор сам определит тип, если вы используете var:

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

На практике для десктопного приложения с xray-core это выглядит так:

Используйте const для всех констант конфигурации (порты, пути, названия), final — для объектов, инициализируемых при запуске, и var — только когда тип очевиден из контекста.

Колекции: List, Map, Set

Для работы с конфигурациями xray-core вам понадобятся коллекции. Вот три основных типа:

Коллекции в Dart имеют богатый набор методов, которые экономят десятки строк кода:

Null Safety: защита от null-ошибок

Null Safety — система, которая на уровне компиляции не позволяет использовать переменную, которая может быть null, без явной проверки. Это критично для десктопного приложения: один необработанный null — и окно приложения зависнет или вылетит.

В Dart по умолчанию переменные не могут быть null:

Есть несколько способов безопасной работы с nullable-типами:

Параллельное выполнение нескольких Future — через Future.wait:

Классы, наследование и mixin

Dart — объектно-ориентированный язык с одиночным наследованием и mixin-ами. Для xray-проекта это означает чистую иерархию протоколов:

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

Перечисления (enum) и расширения (extension)

Для протоколов и настроек xray-core enum — идеальный инструмент:

Enhanced enum (Dart 3+) позволяет добавлять поля и методы прямо в enum:

Все эти конструкции — фундамент, на котором строится Flutter-приложение. Типизация ловит ошибки на этапе компиляции, Null Safety защищает от падений, а асинхронность позволяет управлять процессом xray-core, не блокируя интерфейс. В следующей статье мы соберём из этих блоков визуальный интерфейс на Flutter с Material 3.

2. Верстка интерфейса и Material 3 компоненты

Верстка интерфейса и Material 3 компоненты

Зачем пользователю вашего xray-клиента красивый интерфейс, если «под капотом» и так всё работает? Потому что десктопный софт конкурирует не только функционалом, но и удобством: окно, которое нельзя ресайзить, неинформативные кнопки и отсутствие тёмной темы — причины, по которым пользователь уйдёт к конкуренту. Flutter с Material 3 даёт инструменты, чтобы сделать приложение одновременно функциональным и современным. Разберём, как это работает.

Stateless и Stateful виджеты: когда что использовать

Каждый элемент интерфейса во Flutter — виджет. Их две категории:

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

StatefulWidget — виджет, который может меняться во время работы приложения. Хранит состояние в объекте State. Используйте для переключателей, полей ввода, индикаторов загрузки.

> Правило: если виджет что-то отображает и не меняется — StatelessWidget. Если при нажатии, вводе или таймере что-то обновляется — StatefulWidget. В следующей статье мы заменим StatefulWidget на Riverpod, но для понимания основ — начните с этого.

Column, Row, Stack: три кита верстки

Flutter не использует XML-разметку или CSS. Верстка строится композицией виджетов. Три основных инструмента:

Column — располагает детей вертикально, Row — горизонтально, Stack — друг на друге (по оси Z).

Stack полезен для наложения элементов — например, бейдж поверх иконки:

Flexible, Expanded и SizedBox: контроль размеров

Без явного указания размеров Column и Row распределяют пространство по размеру детей. Для контроля используйте:

  • Expanded — занимает всё свободное пространство
  • Flexible — занимает пространство, но может быть меньше
  • SizedBox — фиксированный размер или spacer
  • Material 3: тема и компоненты

    Material 3 — актуальная дизайн-система от Google. Flutter поддерживает её из коробки. Главное отличие от Material 2 — динамические цвета на основе ColorScheme:

    Все компоненты Material 3 автоматически подхватывают цвета из ColorScheme. Вот основные виджеты, которые пригодятся для xray-клиента:

    | Компонент | Назначение | Пример использования | |-----------|-----------|---------------------| | NavigationBar | Нижняя навигация | Переключение между «Серверы», «Настройки», «Логи» | | NavigationRail | Боковая навигация (десктоп) | Широкие окна на десктопе | | FilledButton | Основное действие | Кнопка «Подключиться» | | OutlinedButton | Вторичное действие | Кнопка «Проверить пинг» | | Card | Карточка данных | Карточка сервера | | SwitchListTile | Переключатель | Включение/выключение автозапуска | | AlertDialog | Модальное окно | Подтверждение удаления сервера | | SnackBar | Уведомление | «Подключение установлено» | | LinearProgressIndicator | Индикатор загрузки | Подключение к серверу |

    Полный экран приложения: Scaffold + NavigationRail

    Для десктопного приложения NavigationBar (мобильный паттерн) не подходит — нужен боковой NavigationRail. Вот полноценный экран:

    Адаптивная верстка: широкий и узкий экран

    Десктопное приложение может быть развёрнуто на полный экран или сжато в узкое окно. Используйте LayoutBuilder для адаптации:

    Работа с темой и текстовыми стилями

    Material 3 определяет иерархию текстовых стилей через TextTheme. Используйте их вместо ручного задания TextStyle — так текст будет корректно адаптироваться при смене темы:

    Для тёмной/светлой темы не задавайте цвета вручную — используйте colorScheme:

    Так при переключении темы все цвета обновятся автоматически.

    Верстка во Flutter — это композиция простых виджетов в сложные интерфейсы. Column, Row и Stack покрывают 95% случаев, а Material 3 даёт готовые компоненты с правильным дизайном. В следующей статье мы научимся управлять состоянием приложения через Riverpod — без setState и с чистой архитектурой.

    3. Управление состоянием с Riverpod и кодогенерация

    Управление состоянием с Riverpod и кодогенерация

    Почему ваш xray-клиент зависает при переключении сервера? Скорее всего, проблема в неэффективной перерисовке виджетов. setState обновляет весь виджет-дерево от точки вызова, а в десктопном приложении с логами, списком серверов и графиком трафика это десятки перерисовок в секунду. Riverpod решает эту проблему: он обновляет только те виджеты, которые реально зависят от изменившихся данных. Разберём, как это работает на практике.

    Установка и базовая настройка

    Добавьте зависимости в pubspec.yaml:

    Оберните приложение в ProviderScope — это контейнер для всех провайдеров:

    Provider, FutureProvider, StreamProvider: три типа данных

    Provider — синхронное значение, которое не меняется. Для констант и конфигураций:

    FutureProvider — асинхронное значение, загружается один раз. Для чтения конфигурации, проверки версии xray-core:

    StreamProvider — поток данных. Для логов xray-core, мониторинга трафика:

    StateNotifier: управляемое изменяемое состояние

    Для списка серверов, статуса подключения и настроек нужен StateNotifier — объект, который хранит состояние и предоставляет методы для его изменения:

    Запустите генерацию:

    После генерации используйте провайдер так:

    Обратите внимание: с кодогенерацией имя провайдера — просто connectionProvider (без ручного объявления), а состояние — ConnectionStatus напрямую, без обёртки в отдельный класс.

    AsyncNotifier: асинхронное состояние

    Для загрузки списка серверов из файла или API используйте AsyncNotifier — он автоматически оборачивает состояние в AsyncValue с тремя состояниями: data, loading, error:

    Riverpod с кодогенерацией превращает управление состоянием из рутинной задачи в декларативное описание: «вот данные, вот способ их изменить, вот виджет, который на них подписан». В следующей статье мы займёмся спецификой десктопа — управление окном, системным треем и запуском процесса xray-core.

    4. Десктопная интеграция: window_manager, tray и запуск процессов

    Десктопная интеграция: window_manager, tray и запуск процессов

    Почему ваш Flutter-клиент для xray-core должен вести себя как настоящее десктопное приложение, а не как мобильное приложение, случайно оказавшееся на Windows? Потому что пользователи десктопа ожидают: сворачивание в трей, кастомный заголовок окна, автозапуск, корректное завершение процесса xray при закрытии. Без этих вещей приложение выглядит как прототип. Разберём, как реализовать каждый из этих элементов.

    window_manager: контроль над окном

    Пакет window_manager даёт программный контроль над окном приложения: размер, положение, заголовок, поведение при закрытии.

    Добавьте зависимости:

    Инициализация должна происходить до запуска приложения:

    setPreventClose(true) — ключевой вызов. Он не даёт окну закрыться мгновенно, а вместо этого вызывает событие onWindowClose, которое вы обрабатываете. Это нужно для: сворачивания в трей, подтверждения выхода, корректной остановки xray-core.

    Перехват закрытия окна

    Используйте WindowListener для обработки событий окна. Лучше всего подключить его через ConsumerStatefulWidget:

    Системный трей: сворачивание и быстрые действия

    Системный трей (system tray) — это иконка рядом с часами. Пользователь может через неё открыть приложение, подключиться/отключиться или выйти.

    Интеграция с основным приложением через Riverpod:

    Запуск и управление процессом xray-core

    Процесс xray-core — это внешний исполняемый файл, который ваше Flutter-приложение запускает, мониторит и останавливает. Вот полноценный менеджер процесса:

    Для парсинга и генерации ссылок (vless://, vmess://, trojan://) библиотека предоставляет класс V2RayShareFormatter:

    Автозапуск при загрузке системы

    Для Windows используйте запись в реестр, для Linux — .desktop файл в ~/.config/autostart/:

    Десктопное приложение — это не просто мобильное приложение на большом экране. Это управление окном, системный трей, работа с процессами и интеграция с ОС. Каждый элемент из этой статьи — window_manager, system_tray, Process.start — превращает Flutter-приложение в полноценный десктопный софт. В следующей статье мы займёмся данными: HTTP-запросы, сериализация JSON, навигация через go_router и автоматическая сборка через GitHub Actions.

    5. Работа с данными, навигация и сборка проекта

    Работа с данными, навигация и сборка проекта

    Как превратить набор экранов в настоящее приложение с историей переходов, сохранением настроек и автоматической сборкой .exe при каждом коммите? Именно этим вопросом мы займёмся в финальной статье. Здесь всё сходится: HTTP-запросы к API xray-core, сериализация JSON-конфигураций, маршрутизация через go_router и CI/CD пайплайн на GitHub Actions.

    dio: HTTP-клиент для работы с Xray API

    Xray-core exposes Stats API на локальном порту (по умолчанию 62789). Через него можно получить статистику трафика, управлять inbounds/outbounds и проверять статус. Пакет dio — наиболее полный HTTP-клиент для Dart:

    Настраиваем клиент и делаем запросы к API xray-core:

    Интеграция с Riverpod:

    Сериализация JSON: ручной подход и кодогенерация

    Конфигурации xray-core — это JSON. Для их обработки есть два подхода.

    Ручной — для простых структур:

    Кодогенерация через json_serializable — для сложных вложенных структур:

    Запустите dart run build_runner build — и получите автоматические fromJson/toJson с поддержкой вложенности, списков и nullable-полей.

    shared_preferences: локальное хранение настроек

    Для сохранения пользовательских настроек (выбранный сервер, автозапуск, тема) используйте shared_preferences. На десктопе данные хранятся в JSON-файле в директории данных приложения:

    Riverpod-интеграция с AsyncNotifier для реактивных настроек:

    go_router: декларативная навигация

    Пакет go_router — рекомендуемый способ навигации во Flutter. Он декларативный (как веб-роутинг), поддерживает deep links и работает с Riverpod:

    ShellRoute оборачивает дочерние маршруты в общий layout (с NavigationRail), а NoTransitionPage убирает анимацию переключения — на десктопе мгновенные переходы выглядят естественнее.

    Использование в приложении и навигация:

    HomeShell — виджет-обёртка с NavigationRail, который определяет текущий индекс по URL:

    GitHub Actions: автоматическая сборка .exe

    Для автоматической сборки при пуше в main создайте .github/workflows/build.yml:

    Для создания GitHub Release с .exe при создании тега добавьте:

    Создание тега и релиза:

    GitHub Actions автоматически соберёт .exe и .tar.gz, и прикрепит их к релизу на странице Releases.

    Структура финального проекта

    Все статьи курса складываются в такую структуру:

    Каждый файл в этой структуре — результат конкретной статьи курса: Dart-основы дают типизацию и асинхронность, Material 3 — верстку, Riverpod — управление состоянием, десктопные пакеты — интеграцию с ОС, а dio + go_router + GitHub Actions — завершённый продукт, который собирается и распространяется автоматически.