Немного обидно терять наработки и кодовую базу, оставшиеся после не слишком удачного эксперимента, однако любые изменения в существующем коде выглядят по ощущениям более трудоёмкими в сравнении с классическими десктопными тулкитами из-за DSL Flutter-а. Отсюда приходит мысль отсоединить Flutter и поставить на его место другой тулкит. GTK выглядит здесь наиболее подходящим претендентом, учитывая лёгкость создания биндингов и возможность аналогий с объектным апи GtkD, которое синтаксически не сильно расходится с синтаксисом Dart.
С одной стороны, биндинги на Dart намного тяжелее как создать, так и использовать в силу высокой абстракции языка, что чревато многочисленными коварными багами, но в рамках эксперимента эти риски минимальны. Кроме того, инфраструктура почти целиком завязана на Flutter, как и значительная часть библиотек, которые проблематично использовать в других тулкитах. С другой стороны, даже частично работающее приложение может делать что-то полезное, а польза в связке Dart и GTK может быть самой разной:
- Биндинги к Gtk.Builder позволяют использовать описание вью в xml и билдер Glade, что сэкономит время на последующих (и особенно глобальных) изменениях макета.
- Запуск GTK на Dart VM выглядит аналогом PyGTK, что удобно для скриптовых и очень простых вспомогательных приложений.
- Возможность использования в одной кодовой базе двух разных тулкитов для мобайла и десктопа, каждый из которых специализирован: юзабилити мобайла сильно отличается от десктопного, что может вносить разного рода неудобства.
- Опыт работы с dart:ffi, учёт архитектурой приложения двух разных тулкитов и т.п.
Поскольку архитектура приложения уже предполагает независимость от интерфейса, то удаление из него Flutter сложностей не представляет, задача сводится к двум основным направлениям: получению биндингов к библиотекам и интеграции тулкита в событийную систему Dart.
Готовых биндингов я не нашёл. На гитхабе и хабре есть несколько примеров, достаточных для старта, однако несмотря на относительно свежие репозитории, апи всё равно уже несовместимо и много где поломано. Это может намекать на серьёзный риск из-за ffi-библиотек и уменьшение количества кода в биндингах, чтобы не пришлось потом его постоянно переделывать.
В примерах была найдена подсказка о работе с UTF8 строками через пакет dart:ffi, а также ffigen для помощи в генерации биндингов. Генерация в лоб приводит к огромным файлам на десятки тысяч строк, что, вероятно, сделает приложение более хрупким из-за изменений в ffi-библиотеках и апи, как отмечалось выше, поэтому я решил большую часть биндингов делать вручную, оставлять только самое необходимое, упрощая апи, прибегая к ffigen для генерации перечислений, констант, структуры классов и т.п. вспомогательных задач. С другой стороны, в автоматическую генерацию временами закрадываются ошибки, например, типы у GdkEventButton сгенерировались неправильно и на полях класса оказался мусор.
Черновой прототип получился каким-то таким, Dart-приложение работает на Dart VM в Linux:
Запуск просто GTK-окна на виртуальной машине занимает считанные секунды, самого приложения в районе 5 секунд или около того, но всё равно вполне терпимо, как и потребление памяти. Приложение успешно компилируется (и работает) в нативный исполняемый файл (ELF) через dart compile, запуск становится почти мгновенным.
В целом биндинги можно свести к концепту GtkD: классам-обёрткам над Pointer с указателем на GTK-структуру. Повсеместное вынесение typedef-ов из параметризации lookupFunction в условиях нестабильного апи не слишком удобно из-за постоянных правок этих двух взаимосвязанных между собой объявлений. С другой стороны, на более устоявшемся коде можно снизить дублирование, а в тех же сигналах без него не обойтись.
Мне потребовались следующие биндинги:
- Glib. Для работы с GObject, сигналами, управления памятью (g_malloc, g_free и т.п.). Часть апи GTK предполагает ручное управление памятью и если управлять ей через аллокаторы ffi апи, то разные типы аллокации запросто могут смешаться.
- Gdk. Разные вспомогательные типы.
- Gtk. Основное апи и контролы (что касается Gio, то функции в Gio.Application можно достать и через Gtk.Application).
- GtkSourceView. Для подсветки синтаксиса JSON-конфига адресов для парсера и окна лога. Об этой библиотеке будет отдельная статья.
- GdkPixbuf. Для работы с изображениями.
Апи основных контролов по большей части простое, построение его тривиально. Вопросы возникают с обработкой событий виджетов: ffi вводит ограничение на коллбэк - статическую функцию на уровне пакета, поэтому я встречал лишь связку её с отдельным статическим StreamController (broadcast), на который подписываются все виджеты, а каждый получает свои события через фильтр (можно сравнить приватный указатель на GTK-структуру с указателем в событии). Способ нельзя назвать удобным, но он выглядит работоспособным в простых случаях.
Ещё одна проблема в интеграции тулкита в event-loop Dart. Запуск приложения через g_application_run блокирующий, вынос логики GTK в отдельный изолят всё равно проблемен из-за неработоспособности входящих событий (т.е. от контроллера в gtk-изолят, слушатель у ReceivePort не срабатывает, ручной поллинг через isEmpty, first и т.п. тоже, ибо они асинхронные), когда как исходящие будут работать. Был найден сниппет с обходным путём через g_main_context_iteration. Если поместить опрос событий gtk в какой-нибудь Dart-цикл (пробовал Timer), то всё начинает работать, хотя бы и с небольшим оверхедом по использованию CPU, вероятно, это можно настроить и получше.
Для отслеживания событий org.freedesktop.NetworkManager и сети использовал dbus по аналогии с RSS-ридером. Бонусом в Dart присутствуют и другие биндинги от Canonical.
Каких-либо других проблем с биндингами или самим Dart я не заметил, основная логика приложения осталась без изменений, поменялся только графический интерфейс. Однако упрощённая двухканальная архитектура сумела вывезти смену графического тулкита, возможно, при более плотных экспериментах с Flutter я также присмотрюсь к этому варианту, а может быть возьму и какой-то более специализированный вариант архитектуры из мобайла или их гибрид.
Несмотря на относительную трудоёмкость затеи и остаточное количество плавающих багов, эксперимент можно признать частично успешным: приложение успешно выполняет свои задачи, однако отсутствие пакета с биндингами для такого монструозного тулкита как GTK сильно замедляет разработку и создаёт почву для коварных багов.
Это намекает на более узкий класс задач, учитывающий, например, богатый выбор библиотек в Dart и относительную простоту интерфейса, как и другие факторы, где связку Dart с GTK вполне возможно и удобно использовать. В ином же случае проще взять Flutter или же другой тулкит, собственно, как и всегда: каждой задаче - свой инструмент.