
Привет, Хабр! Я — Роза, Flutter-разработчица в Friflex. Уверена, многие из вас знакомы с Dart DevTools и уже использовали его для анализа своих Flutter-приложений. Но пробовали ли вы создавать собственные расширения? Недавно у меня была такая задача, и я хочу поделиться своим опытом.
Что нужно делать
Первым делом создаем новый пакет для нашего расширения. Это можно сделать при помощи команды flutter create --template app --platforms web my_dev_tools_ext. Официального шаблона для DevTools extensions нет, структура добавляется вручную.
Подключаем пакет devtools_extensions. В нем есть инструменты, чтобы создать собственное расширение: доступ к виртуальной машине, темам, виджетам и другим возможностям DevTools.
Например, вы получите доступ к менеджерам:
extensionManager— взаимодействие с DevTools;serviceManager— доступ к VM (если подключена);dtdManager— связь с Dart Tooling Daemon.
Создаем папку devtools в корне пакета, и в ней — папку build. Добавляем файл конфигурации config.yaml.
my_dev_tools_ext/ extension/ devtools/ build/ config.yaml lib/ src/ ...
Файл config.yaml:
name: my_dev_tools_ext issueTracker: https://... version: 0.0.1 materialIconCodePoint: '0xe0b1' requiresConnection: false
Описание полей:
name: имя пакета, к которому относится расширение DevTools. Значение этого поля будет использоваться для расширения во вкладке DevTools.
issueTracker: URL-адрес системы отслеживания ошибок расширения. Когда пользователь нажмет «Сообщить об ошибке» в пользовательском интерфейсе DevTools, он будет перенаправлен на этот URL-адрес.
version: версия расширения DevTools. Значение этого поля будет использоваться в заголовке страницы расширения.
materialIconCodePoint: этот значок будет использоваться для вкладки расширения на панели вкладок верхнего уровня DevTools.
Необязательные поля:
requiresConnection: требуется ли для запуска этого расширения DevTools подключенное приложение Dart или Flutter. По умолчанию — false.
В статье я сфокусировалась на практическом опыте и не охватила все возможности пакета. Если планируете использовать DevTools extensions в работе, обязательно загляните в документацию — там есть дополнительные API и сценарии, которые могут пригодиться в более сложных кейсах.
Теперь создаем UI. Кроме devtools_extensions, очень полезным будет пакет devtools_app_shared. Он содержит готовые компоненты и утилиты, которые используются в оригинальных DevTools.
Например (для кнопки):
DevToolsButton( onPressed: () async { await someService.saveData(); extensionManager.showNotification('Данные успешно сохранены!'); }, icon: Icons.save, label: 'Сохранить', )
extensionManager.showNotification покажет уведомление. В симулированной среде это будет лог в консоли.
Подключение и тестирование расширения
Добавим обертку DevToolsExtension, которая инициализирует расширение:
void main() { runApp(const LocalizationDevToolsExtension()); } class LocalizationDevToolsExtension extends StatelessWidget { const LocalizationDevToolsExtension({super.key}); @override Widget build(BuildContext context) { return const DevToolsExtension( child: LocalizationSnapshotterWidget(), // ваш основной виджет ); } }
Чтобы протестировать расширение, можно воспользоваться симулированной средой. Для этого нужно запустить команду из корня пакета расширения:
flutter run -d chrome --dart-define=use_simulated_environment=true
Если все работает корректно, соберите расширение:
dart run devtools_extensions build_and_copy --source=. --dest=../extension/devtools
После этого в devtools/build/ появится сборка, которая готова к публикации или локальному использованию.
Чтобы проверить, корректно ли настроено расширение и будет ли оно загружаться в DevTools, можно воспользоваться командой validate.
Она проверяет структуру пакета и наличие необходимых файлов.
dart run devtools_extensions validate --package=../my_dev_tools_ext
Это особенно полезно, если расширение не появляется в DevTools.
Как работают расширения DevTools
Все просто: расширение — это обычный Dart-пакет. Вы можете встроить его в другой pub-пакет или создать отдельный. Чтобы расширение появилось в интерфейсе DevTools, его нужно подключить как зависимость в проекте, где DevTools используются.
Какие инструменты можно создавать
С помощью Flutter DevTools extensions можно реализовывать разные типы инструментов:
вспомогательные инструменты для существующих пакетов (например, дебаг или визуализацию состояния);
отдельные инструменты, распространяемые как самостоятельные пакеты;
инструменты, которые взаимодействуют с запущенным приложением через VM Service;
инструменты, которые не требуют подключения к приложению (например, анализ конфигурации или данных);
инструменты, которые работают с файлами проекта (в связке с IDE).
Какие бывают DevTools extensions
В Flutter DevTools есть два основных типа расширений: standalone extensions и companion extensions.
Standalone extensions
Это расширения, которые разрабатываются как отдельные пакеты и не привязаны к существующему pub-пакету.
Исходный код расширения можно хранить в том же репозитории, что и основной проект, и подключать его как dev_dependency.
О создании standalone-расширения я рассказывала выше.
Companion extensions
Это расширения, которые поставляются вместе с существующим pub-пакетом.
Когда пользователь подключает такой пакет к своему приложению, DevTools автоматически обнаруживает расширение и добавляет новую вкладку.
На практике выбор между standalone и companion зависит от того, насколько расширение связано с вашим пакетом. Если это вспомогательный инструмент для разработки — лучше standalone, если часть пользовательского опыта — companion.
Дальше я расскажу, как создать companion-решение.
Как встроить расширение в существующий pub-пакет
Допустим, у вас уже есть пакет с реализованным функционалом, для которого вы хотите сделать DevTools-расширение. Нет смысла создавать отдельную зависимость только ради расширения. Проще сделать расширение частью этого же пакета.
Например, когда пользователь подключает package:some_package к своему приложению, он автоматически получает доступ к расширению DevTools, встроенному в этот пакет. DevTools при запуске определит наличие расширения и добавит новую вкладку для него.
Как это реализовать? Все довольно просто. В Dart-пакете, который предоставляет расширение DevTools, нужно добавить каталог extension на верхнем уровне структуры:
some_package/
├── extension/
├── lib/
└── ...
Структура папки extension будет выглядеть так:
extension/
└── devtools/
├── build/
└── config.yaml
Вы получите примерно такую структуру:
some_app/└── packages/ ├── some_package/ │ └── extension/ │ └── devtools/ │ ├── build/ │ │ └── ... │ └── config.yaml └── some_devtools_extension/ └── lib/
Если функциональность расширения и самого пакета никак не связаны, это может запутать архитектуру. Тогда лучше вынести расширение в отдельный pub-пакет и подключать его как dev_dependency.
Если вы не собираетесь использовать расширение снова и оно должно быть автономным, его можно разместить в том же репозитории, что и основной пакет. Но как отдельный модуль. Это упростит разработку и при подключении через dev_dependency не повлияет на размер конечного пользовательского приложения.
Что такое EvalOnDartLibrary
Это класс из пакета devtools_app_shared, который позволяет выполнять Dart-код прямо из вашего DevTools extension. С его помощью можно управлять состоянием приложения, вызывать методы, получать значения из рантайма.
Для меня это был довольно неожиданный инструмент. Я не сразу поняла, что из расширения DevTools вообще можно настолько глубоко взаимодействовать с приложением.
Как это работает
Чтобы использовать EvalOnDartLibrary, сначала нужно инициализировать его и передать все необходимое для работы с vmService.
Future<void> initEval() async { await serviceManager.onServiceAvailable; _controllerEval = EvalOnDartLibrary( 'package:some_package/src/controller.dart', // Путь к библиотеке, где находится нужный код serviceManager.service!, // Передаем vmService (Убедитесь, что serviceManager.service не равен null (после onServiceAvailable) serviceManager: serviceManager, ); evalDisposable = Disposable(); // Обязательно создаем Disposable }
'package:...' — это путь до библиотеки, в которой находится нужный код;
serviceManager.service! — экземпляр VmService;
Disposable нужен, чтобы корректно управлять жизненным циклом и не оставлять лишние подписки или вызовы после закрытия DevTools.
Убедитесь, что serviceManager.service не равен null (после onServiceAvailable).
Какие методы есть у EvalOnDartLibrary
asyncEval — асинхронное выполнение кода;
eval — выполнение выражения;
evalInstance — получение экземпляра объекта для дальнейшей работы с его полями и методами;
safeEval — более безопасная обертка над eval, которая дополнительно обрабатывает ошибки выполнения.
На практике можно сначала выполнить нужное действие, а затем отдельно получить результат из рантайма. Например, вызвать метод и получить значение.
У eval и asyncEval есть ограничение на размер возвращаемых данных. Поэтому удобнее сначала вызвать метод через asyncEval или eval, а потом получить нужное значение через evalInstance.
Это может выглядеть так:
Future<String?> _getValue() async { await _controllerEval.asyncEval( 'await SomeController.instance.calculateValue()', // Запуск асинхронной функции isAlive: evalDisposable, ); final result = await _controllerEval.evalInstance( 'SomeController.instance.sum.value', // Получение значения после выполнения isAlive: evalDisposable, ); // Результатом является объект типа Instance return result?.valueAsString; // Возвращаем строковое значение }
Здесь сначала вызывается асинхронный метод calculateValue(), а потом из рантайма читается уже обновленное значение SomeController.instance.sum.value. Еще важно не забывать передавать isAlive: evalDisposable.
Особенности работы EvalOnDartLibrary
При работе с EvalOnDartLibrary стоит учитывать несколько важных моментов.
Код выполняется в контексте текущего isolate, поэтому доступ к объектам и библиотекам ограничен тем, что реально загружено в рантайме.
Из-за этого могут возникать нюансы:
код может быть недоступен из-за tree shaking;
не всегда можно обратиться к приватным классам и полям;
возможны ограничения при использовании deferred imports.
Что такое Instance
Метод evalInstance возвращает Instance — ссылку на результат выполнения кода.
Дальше это значение можно преобразовать, например, в строку через valueAsString. Но важно понимать, что не каждое значение нужно безусловно приводить к строке. В зависимости от результата могут понадобиться дополнительные проверки.
Например:
final result = await _controllerEval.evalInstance( 'SomeController.instance.sum.value.toString()', isAlive: evalDisposable, ); final value = result?.valueAsString;
Правила работы с EvalOnDartLibrary
На практике это детали, из-за которых все либо быстро начинает работать, либо неожиданно ломается в самый неподходящий момент.
Используйте Disposable для управления жизненным циклом;
Учитывайте ограничение на размер результата;
Передавайте только валидные Dart-выражения;
При изменениях в проекте не забывайте обновлять пути в EvalOnDartLibrary.
Вывод
Для меня это был неплохой опыт с DevTools: я даже не знала, что так можно.
А еще через serviceManager можно получить код приложения через id изолята, но там уже есть свои нюансы, расскажу о них позже.
А вы уже пробовали писать свои DevTools extensions? Расскажите о своем опыте в комментариях!
