Как написать модуль виджета
Модуль виджетов это обычный проект на Kotlin и Compose, собирающийся в jar. Лаунчер находит его в папке виджетов при запуске и спрашивает у него виджеты. Против модуля лаунчер не компилируется.
Полностью рабочий пример лежит в examples/widget-pixelplayer. Он рисует всё сам и не зависит ни от чего в лаунчере, кроме ядра виджетов. Ваш модуль должен выглядеть так же.
Виджет это composable-функция верхнего уровня с одним параметром:
@Widget(id = "example.clock", displayName = "Часы")@Composablefun ClockWidget(instance: WidgetInstance) { Text(remember { LocalTime.now().toString() })}id должен быть уникален среди всего, что работает в этом лаунчере, включая чужие модули. Ставьте впереди id своего модуля. Столкновение решается отбрасыванием более позднего, так что совпадение со встроенным виджетом означает, что ваш просто никогда не появится. В лог при этом пишется, чей виджет проиграл.
Необязательные аргументы @Widget:
displayName– как виджет называется в редакторе. По умолчанию имя функции.removable– поставьте false, чтобы спрятать кнопку удаления. Для виджетов, без которых поверхность ломается.slots– идентификаторы мест, куда можно класть вложенные виджеты. Только для контейнеров.propsClass–@Serializabledata-класс с настройками. У каждого поля должно быть значение по умолчанию. Редактор строит по нему форму, виджет читает значения черезinstance.rememberProps<T>().surface– подложка, на которой сидит виджет, какSurfaceSpecв том же JSON, что несёт файл раскладки. Пусто (по умолчанию) означает, что подложки нет: виджет рисует своё содержимое и ничего за ним. Процессор разбирает строку на сборке и роняет её на некорректном значении. Объявляйте подложку здесь, а не рисуйте сами, и тогда пользователь сможет менять её форму из редактора.drawsOwnSurface– true для виджета, который рисует свою подложку сам, в теле. Пустойsurfaceозначает «подложки нет, и её можно добавить», а это – «подложка есть, и нарисовало её не ядро», поэтому редактор перестаёт предлагать вторую поверх. Нужен только там, где запись не может описать подложку: форма, которая анимируется, или меняется вместе с состоянием самого виджета.
Неправильная сигнатура ломает сборку с диагностикой, а не всплывает в рантайме.
От любого другого проекта на Compose модуль виджетов отличают две вещи.
Назовите свой реестр. Процессор аннотаций генерирует объект-реестр, и два модуля с одинаковым полным именем столкнутся на classpath: один потеряет свои виджеты, и никто об этом не скажет. Возьмите имена, которые никто не займёт:
ksp { arg("widgetRegistryPackage", "com.example.mymodule.generated") arg("widgetRegistryName", "MyModuleWidgetRegistry")}Представьтесь в манифесте. Это то, что лаунчер читает раньше, чем откроет хоть один класс:
tasks.jar { manifest { attributes( "Nexira-Widget-Api" to 1, "Nexira-Module-Id" to "mymodule", "Nexira-Module-Name" to "My Module", ) }}Nexira-Widget-Api должен совпадать с hivens.widget.api.WidgetApi.VERSION того лаунчера, под который вы собираетесь. Модуль с другим числом не будет загружен, и в лог попадут оба числа. Так сделано намеренно: виджет несёт в себе сгенерированные компилятором вызовы в рантайм Compose, и модуль, собранный против другого рантайма, может слинковаться и вести себя неправильно. Разбираться с этим дороже, чем с модулем, который не загрузился по названной причине.
Nexira-Module-Id это то, под каким именем вы видны в логах, а со временем и в списке модулей внутри лаунчера. Не меняйте его от версии к версии.
Файл сервисов, по которому лаунчер вас находит, генерируется сам в META-INF/services. Писать его руками не нужно.
Зависимости
Заголовок раздела «Зависимости»Зависьте от ядра и от того, что вам действительно нужно. Чего делать нельзя, так это рассчитывать, что будет использована ваша копия общего рантайма:
dependencies { api(project(":widget-model")) api(project(":widget-api")) implementation(libs.compose.runtime) implementation(libs.compose.foundation) implementation(libs.compose.ui)}Каждый модуль загружается собственным загрузчиком классов поверх лаунчерского, и делегирование идёт сначала родителю. Если ваш jar притащит с собой Compose, стандартную библиотеку Kotlin или widget-api, использованы будут копии лаунчера, а ваши проигнорированы. Именно это заставляет composable в вашем модуле разговаривать с тем же рантаймом Compose, что и всё остальное. По-настоящему свои зависимости, которых у лаунчера нет, грузятся из вашего jar как обычно.
Модули не видят классов друг друга. Два модуля могут нести класс с одинаковым именем и не мешать друг другу.
Пользоваться дизайн-системой лаунчера вы не обязаны, и пример намеренно ей не пользуется: он объявляет собственную палитру и рисует собственные элементы. Оба пути нормальны.
Установка во время работы
Заголовок раздела «Установка во время работы»tasks.register<Copy>("installWidget") { from(tasks.jar) into(providers.gradleProperty("widgetsDir").orElse( providers.systemProperty("user.home").map { "$it/.local/share/nexira/widgets" }, ))}Дальше ./gradlew installWidget и перезапуск лаунчера. Модули сканируются один раз при старте, так что каждая правка стоит перезапуска.
Чего лаунчер не делает
Заголовок раздела «Чего лаунчер не делает»Песочницы нет. Ваш модуль работает с тем же доступом, что и лаунчер: файлы, сеть, всё, до чего дотягивается JVM. Лаунчер это не ограничивает и ничего не спрашивает у пользователя. Вместо этого он называет ваш модуль в логе, а со временем и в списке, который пользователь увидит и сможет выключить.
Ничего не подписывается и не проверяется. Модулю доверяют потому, что человек сам положил файл.
Распространение
Заголовок раздела «Распространение»Прикладывайте jar к релизу в своём репозитории. Магазина, куда надо подавать заявку, нет, и одобрения тоже. Общий каталог модулей запланирован; пока его нет, способ распространения это ссылка.