Назад к блогу

Берём Glance Widgets под контроль

Android
15 мая 2023
8 мин

Берём 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.

Вернуться к списку статей