Берём Glance Widgets под контроль
Введение
В этой статье я покажу, как создавать stateful виджеты для Android через Glance Compose. Вы узнаете, как обновлять каждый экземпляр отдельно от остальных и управлять ими из приложения.
Glance библиотека
Glance входит в семейство Jetpack и позволяет создавать виджеты через Compose Runtime вместо устаревшего RemoteViews. Это значительно упрощает разработку дизайна и взаимодействие с виджетами.
Преимущества
Главное достоинство библиотеки — GlanceStateDefinition. Каждый экземпляр виджета получает собственное хранилище на основе DataStore.
StateDefinition — это хранилище на основе DataStore, которое по-сути просто файл.
Система обновления позволяет обновлять все виджеты сразу или выборочно через предикаты.
Ограничения
К сожалению, у Glance есть ряд ограничений:
- Ограниченный набор Composable функций (Box, Row, Column, Text, Button, LazyColumn, Image, Spacer)
- Отсутствие поддержки MaterialTheme
- Нет механизма remember как в обычном Compose
- Требуется XML метаданные
- Нет поддержки кастомных шрифтов
Архитектурные компоненты
Для работы с Glance нужно понимать 4 основных компонента:
- GlanceAppWidgetManager — управляет экземплярами виджетов
- GlanceAppWidgetReceiver — обрабатывает обновления виджетов
- GlanceAppWidget — точка входа через функцию Content()
- PreferencesGlanceStateDefinition — создаёт отдельные хранилища для каждого виджета
Практическая реализация
Давайте создадим приложение блокнота с возможностью добавления виджетов для заметок. Наше приложение будет уметь:
- Добавлять виджеты через лаунчер
- Добавлять виджеты прямо из приложения
- Синхронизировать виджеты при редактировании заметок
- Открывать заметку по клику на виджет
Определение состояния
Сначала определим ключи для хранения состояния виджета:
val noteId = longPreferencesKey("noteId")
val noteTitle = stringPreferencesKey("noteTitle")
val noteText = stringPreferencesKey("noteText")
val noteUpdatedAt = stringPreferencesKey("noteUpdatedAt")
Создание виджета
Создаём класс виджета, наследующийся от GlanceAppWidget:
class NoteWidget : GlanceAppWidget() {
override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
@Composable
override fun Content() {
NoteWidgetContent()
}
}
Регистрация в манифесте
Не забываем зарегистрировать receiver в AndroidManifest.xml:
<receiver
android:name=".widget.NoteWidgetReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_meta" />
</receiver>
Композабл функция виджета
Создаём UI виджета с использованием Glance Composables:
@Composable
fun NoteWidgetContent(prefs: Preferences) {
val noteId = prefs[noteIdPK] ?: Long.MIN_VALUE
val noteTitle = prefs[noteTitlePK].orEmpty()
LazyColumn(modifier = GlanceModifier.padding(16.dp)) {
if (noteTitle.isNotEmpty()) item {
WidgetText(noteTitle, noteId)
}
}
}
Конфигурационная активити
При добавлении виджета через лаунчер нужно сохранить его состояние:
private fun saveWidgetState(id: Long) = lifecycleScope.launch(Dispatchers.IO) {
val glanceId = GlanceAppWidgetManager(applicationContext).getGlanceIdBy(widgetId)
val note = repository.getNote(id)?.let { it.toEntity() } ?: return@launch
updateAppWidgetState(applicationContext, glanceId) { prefs ->
prefs[noteIdPK] = id
prefs[noteTitlePK] = note.title
}
NoteWidget().update(applicationContext, glanceId)
}
Обновление виджетов при редактировании
Когда пользователь редактирует заметку, нужно обновить все связанные виджеты:
suspend fun GlanceAppWidgetManager.mapNoteToWidget(context: Context, note: Note) =
getGlanceIds(NoteWidget::class.java)
.forEach { glanceId ->
updateAppWidgetState(context, glanceId) { prefs ->
if(prefs[noteIdPK] == note.id) {
prefs[noteTitlePK] = note.title
}
}
NoteWidget().updateIf<Preferences>(context) {
it[noteIdPK] == note.id
}
}
Обработка кликов
Добавляем возможность открыть заметку по клику на виджет:
@Composable
fun WidgetText(text: String, noteId: Long) {
Text(
text = text,
modifier = GlanceModifier.clickable(
actionStartActivity<RootActivity>(
parameters = actionParametersOf(
noteIdParam to noteId
)
)
)
)
}
Закрепление виджета из приложения
Позволяем пользователю добавлять виджеты прямо из приложения:
private fun handlePinWidget(noteId: Long) {
val intent = Intent(context, PinWidgetReceiver::class.java)
intent.putExtra(NOTE_ID, noteId)
val pendingIntent = PendingIntent.getBroadcast(
context,
noteId.toInt(),
intent,
PendingIntent.FLAG_IMMUTABLE
)
GlanceAppWidgetManager(context).requestPinGlanceAppWidget(
NoteWidgetReceiver::class.java,
successCallback = pendingIntent
)
}
Заключение
Glance значительно упрощает разработку виджетов для Android через привычный Compose подход. Библиотека предоставляет удобную систему управления состоянием через DataStore, позволяя управлять каждым экземпляром виджета отдельно.
Несмотря на ограничения в виде небольшого набора Composable функций и отсутствия MaterialTheme, Glance отлично подходит для создания функциональных виджетов с минимальными усилиями.
Полный код примера доступен на GitHub.