Почему? Потому что классическая разработка виджетов под Android была откровенно неприятной. RemoteViews, XML-вёрстка, которую нельзя нормально отладить, постоянные проблемы с PendingIntent и ощущение, что ты работаешь не с современным фреймворком, а с каким-то легаси из 2012 года. Всё изменилось, когда я наконец добрался до Jetpack Glance — и провёл с этой библиотекой последние полгода в плотном продакшен-режиме.
В этой статье я поделюсь своим реальным опытом: как я проектировал виджет с нуля, какие грабли собрал по пути и к какой архитектуре в итоге пришёл. Без прилизанных примеров из документации — только то, что работает на реальных устройствах с реальными пользователями.
С чего всё началось
Когда мне в очередной раз прилетела задача на виджет, я мысленно приготовился страдать. В моей голове всплыли воспоминания о том, как я когда-то верстал виджеты на чистом XML: создаёшь appwidget-provider, прописываешь initialLayout, пишешь класс-наследник AppWidgetProvider, и начинается магия бубна. Надо вручную обновлять текст в TextView через RemoteViews.setTextViewText, привязывать обработчики через setOnClickPendingIntent, и каждый раз гадать, как это будет выглядеть на экране пользователя, потому что Preview в Android Studio для виджетов просто не работает.
А потом коллега скинул мне ссылку на библиотеку Jetpack Glance. Я открыл документацию, увидел синтаксис, подозрительно похожий на Jetpack Compose, и моё сердце ёкнуло. Неужели можно будет писать виджеты так же, как я пишу современные UI-компоненты? С Row, Column, Box, с нормальными модификаторами и без вот этого всего XML-ада?
Оказалось — можно. Но дьявол, как всегда, в деталях.
Первое знакомство: что такое Glance на самом деле
Когда я начал копать глубже, я понял важную вещь, которую многие упускают. Glance — это не Compose в привычном понимании. Да, синтаксис похож, да, используются @Composable-функции, да, есть GlanceModifier и компоненты вроде Text, Button, Image. Но под капотом всё работает совершенно иначе.
Обычный Jetpack Compose рендерит UI напрямую на Canvas. Он создаёт дерево нод, измеряет их, располагает и отрисовывает. Glance так не делает. Он транслирует ваши Composable-функции в старые добрые RemoteViews. Это принципиальный момент, который я осознал не сразу, и зря — потому что из него вытекает множество ограничений.
Представьте себе: вы пишете красивый декларативный код на Kotlin, расставляете отступы в dp, используете Spacer, настраиваете шрифты. А библиотека в фоне превращает всё это в плоскую XML-подобную структуру, которую поймёт системный лаунчер. Это гениально с точки зрения совместимости — виджет будет работать даже на старых устройствах, где нет Compose Runtime. Но это же накладывает суровые рамки на то, что мы можем сделать.
Когда я это осознал, моя картина мира пересобралась. Я перестал ожидать от Glance поведения полноценного Compose и начал воспринимать его как то, чем он является на самом деле: высокоуровневую DSL-обёртку над RemoteViews. С этого момента всё встало на свои места.
Собираем проект: зависимости и первые грабли
Я начал с добавления зависимостей. Казалось бы, что может быть проще? Открываю build.gradle, добавляю пару строк — готово. На практике вылезло сразу несколько нюансов.
Первое: Glance требует включённого Compose даже в том модуле, где вы пишете виджет. Даже если всё остальное приложение написано на XML и View-системе. Это значит, что в android-блоке нужно явно прописать:
android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.0"
}
}
На момент моего внедрения актуальной была версия 1.5.0, но это плавающая цифра, и её нужно сверять с таблицей совместимости на сайте Google. Если версия Compose Compiler не совпадёт с версией Kotlin в проекте, вы получите невнятную ошибку компиляции и потратите час на гугление.
Второе: сами зависимости Glance. Мне потребовались две:
dependencies {
implementation("androidx.glance:glance:1.2.0-beta01")
implementation("androidx.glance:glance-appwidget:1.2.0-beta01")
}
Обратите внимание на суффикс -beta01. На момент моего внедрения библиотека ещё не вышла в стабильный релиз, и это создавало определённую нервозность у продактов. Но я решил рискнуть, потому что преимущества перевешивали. За полгода в проде ни одного падения, связанного с Glance, я не поймал.
Третье: минимальная версия API. Glance работает начиная с API 21 (Android 5.0), но полноценная поддержка адаптивных размеров виджетов появляется только с API 31 (Android 12). Если ваша аудитория массово сидит на более старых версиях, придётся закладываться на фиксированные размеры. Я для себя решил так: базовый лейаут делаю для всех, а адаптивность добавляю как прогрессивное улучшение для Android 12+.
Первый виджет: от пустого экрана к рабочему прототипу
С теорией я разобрался, пора писать код. Первое, что нужно сделать, — создать класс виджета. В Glance для этого есть базовый класс GlanceAppWidget. Вы наследуетесь от него и переопределяете метод provideGlance:
class MyAppWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
WidgetContent()
}
}
}
Заметили модификатор suspend? Это было для меня приятным сюрпризом. provideGlance — это корутина, и внутри неё я могу спокойно делать запросы к базе данных или к API, не блокируя главный поток. В классических виджетах мне приходилось запускать фоном IntentService или корутину вручную, а здесь всё из коробки.
Внутри provideContent я разместил свой первый @Composable:
@Composable
private fun WidgetContent() {
Column(
modifier = GlanceModifier
.fillMaxWidth()
.background(R.color.widget_background)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Привет, я виджет!",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = ColorProvider(Color.White)
)
)
Spacer(modifier = GlanceModifier.height(8.dp))
Button(
text = "Открыть приложение",
onClick = actionStartActivity<MainActivity>()
)
}
}
Вроде бы всё как в обычном Compose: Column, Text, Button, Spacer. Но когда я присмотрелся, я заметил отличия. GlanceModifier вместо Modifier. ColorProvider вместо прямого Color. actionStartActivity вместо лямбды onClick. Это не случайные расхождения — они продиктованы архитектурой RemoteViews.
Цвета в виджетах должны передаваться через ColorProvider, потому что RemoteViews не умеют хранить произвольные объекты. Системе нужен сериализуемый формат. ColorProvider как раз и даёт такую сериализацию, причём с поддержкой дневной и ночной темы из коробки. Я могу задать ColorProvider(day = Color.Blue, night = Color.Cyan), и виджет сам перекрасится при смене системной темы.
С обработчиками кликов своя история. В классических виджетах я привык использовать PendingIntent, и Glance никуда от этого не ушёл. actionStartActivity под капотом создаёт PendingIntent, который запускает указанную Activity. Просто синтаксис стал чище.
Манифест и метаданные: за что я споткнулся на ровном месте
Казалось бы, класс виджета написан, интерфейс готов — можно запускать. Но виджет не появляется в списке доступных. Почему? Потому что Android требует, чтобы каждый виджет был зарегистрирован в манифесте и имел XML с метаданными. Об этом легко забыть, если давно не работал с виджетами.
В AndroidManifest.xml я прописал:
<receiver
android:name=".widget.MyAppWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/my_app_widget_info" />
</receiver>
Обратите внимание: в android:name я указал не сам класс виджета, а ресивер. В Glance нужно создать отдельный класс-ресивер, который наследуется от GlanceAppWidgetReceiver:
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье