Привет! Я с разработкой виджетов никогда не пересекался, честно говоря даже забыл о них, но на работе возникла задача - изучить, рассказать и интегрировать виджеты в приложение. После того, как задача была закончена решил поделиться своим опытом. Сталкивался с трудностями, буду рад, если статья поможет их разрешить.

Что такое виджеты и почему они особенные

Android виджеты используют RemoteViews - специальный механизм для отображения UI за пределами своего процесса. RemoteViews это вью, имеющие ограниченную функциональность по сравнению с обычными View, которыми мы привыкли пользоваться. Работа с виджетами с каждой версией улучшалась и в итоге вышел новый подход к проектированию виджетов. Библиотека Glance. Она похожа на декларативную верстку через Compose, но все же стоит понимать, что это не он.

Почему glance это не compose:

  • Ограниченный набор компонентов

  • Отсутствие прямых обработчиков событий

  • Ограниченные модификаторы

  • Отсутствие рекомпозиции

  • Разная система тем и стилей

На самом деле различий намного больше, это только часть из них

Почему нельзя использовать обычные view:

  • Производительность - виджеты должны быть легковесными

  • Виджеты работают в процессе launcher из-за чего нет доступа к твоему приложению. RemoteViews - это механизм Android для кросс-процессного рендеринга UI

  • Согласованность - единый подход для всех виджетов на устройстве.

По сути Glance - это современный API-мост между декларативным стилем Compose и системными ограничениями Android виджетов, которые требуют использования RemoteViews для кросс-процессного рендеринга.

Исторический контекст

Android 1.5 (API 3) - Рождение виджетов

Первая версия виджетов была простой и функционально ограниченной:

  • Отображение статической информации

  • Базовые взаимодействия (открытие приложения)

  • Минимальные возможности кастомизации

Эволюция: от API 3 до Android 11

С каждой версией Android виджеты становились мощнее:

  • Поддержка настройки размеров - гибкая адаптация под сетки

  • Списки и коллекции - ListViewGridView в виджетах

  • Auto-resize - автоматическое масштабирование контента

  • Material Design - современные визуальные стандарты

  • Обновления по расписанию - фоновые работы

Android 12 (API 31) - Революция в мире виджетов

Android 12 принес кардинальные изменения:

  • Новый гибкий API - переработанная архитектура

  • Сложные анимации - плавные переходы и эффекты

  • Кастомные вью - расширенные возможности кастомизации

  • Динамические цвета - адаптация под тему системы

  • Улучшенная производительность - оптимизация рендеринга

Добавление виджета

Мы будем пользоваться новым API для создания виджета, поэтому сразу подключим библиотеку Glance с актуальной версией:

implementation("androidx.glance:glance-appwidget:1.1.1")

После чего у нас появятся все необходимые инструменты.

Далее необходимо сделать важные 4 пункта:

  1. Создать наследника GlanceAppWidget

  2. Создать наследника GlanceAppWidgetReceiver

  3. Создать конфигурационный файл, описывающий для системы настройки виджета

  4. Подключить это все в manifest

GlanceAppWidget

Этот класс будет отвечать за весь UI и взаимодействие с ним. Напишем простой контейнер для начала.

class AppWidget : GlanceAppWidget() {
  override suspend fun provideGlance(context: Context, id: GlanceId) {
    provideContent {
      GlanceTheme {
        
      }
    }
  }
}

И по сути мы можем описывать внутри GlanceTheme UI в таком же декларативном стиле, как и Compose.

GlanceAppWidgetReceiver

Этот класс отвечает за регистрацию твоего виджета в манифесте, позволяет обрабатывать системные события, связанные с твоим виджетом. Так же возвращает экземпляр твоего виджета

Почему его нужно регистрировать в Manifest.xml? Потому что он наследник BroadcastReceiver:

class AppWidgetReceiver : GlanceAppWidgetReceiver() {
  override val glanceAppWidget: GlanceAppWidget = AppWidget()
}

Файл конфигурации виджета

Это глобальный файл с настройками твоего виджета, там можно много чего указать, разберем несколько свойств

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   android:description="@string/app_widget_description"
   android:targetCellHeight="2"
   android:targetCellWidth="2"
   android:resizeMode="vertical|horizontal"
   android:previewLayout="@layout/widget_preview_layout"
   android:previewImage="@drawable/widget_preview_image"
   android:maxResizeWidth="600dp"
   android:maxResizeHeight="120dp"
   android:minResizeWidth="120dp"
   android:minResizeHeight="40dp"
   android:minHeight="120dp"
   android:minWidth="120dp"
   android:updatePeriodMillis="0"
   android:widgetFeatures="reconfigurable" />
  • description - описание виджета, которое пользователь увидит в списке виджетов, когда решит его добавить

  • targetCellHeight/targetCellWidth - новая функциональность для Android 12 (API 31) +. Весь экран представляет собой сетку. Каждая иконка приложения это по сути виджет размера 1х1. Мы можем задать размер виджета, используя сетку

  • updatePeriodMillis - частота обновления виджета. В данном случае мы указываем 0, поскольку за обновление виджета в статье будет отвечать WorkManager

  • widgetFeatures - говорит о том, что у виджета есть возможность изменять настройки. В будущем мы добавим конфигурационную activity, в которой можно будет менять настройки виджета. Эта activity будет вызвана при первом добавлении виджета на экран, а значение reconfigurable говорит о том, что мы можем сколько угодно раз изменять конфигурацию виджета

  • previewLayout/previewImage - image для Android 11 (API 30) -, layout для версий выше. Это изображение, которое видит пользователь, когда находится в списке виджетов. Раньше была просто статическая картинка, теперь это полноценный xml файл, который и размер системы учтет, и dark mode режим и т.д.

Manifest

Соберем это все безобразие в один файл и подключим к нашему приложению

<receiver
   android:name=".widgets.AppWidgetReceiver"
   android:exported="false"
   android:enabled="true"
   android:label="@string/app_widget_name">

   <intent-filter>
       <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
   </intent-filter>

   <meta-data
       android:name="android.appwidget.provider"
       android:resource="@xml/app_widget_provider" />

</receiver>

Я так же опустил здесь подключение конфигурационной activity, но доберемся до этого позже

UI

Теперь мы можем добавить наш виджет на экран. Но, чтобы увидеть какой-нибудь результат - добавим заглушку.

Для начала добавим data class для того, чтобы держать какие-то данные, которые мы можем отобразить. Я создал следующий класс:

data class AppWidgetDataContent(
  @StringRes val titleResId: Int, val description: String, val value: Int
)

Создал из него заглушку и сверстал следующий контент:

@Composable
fun AppWidgetContent(
   state: AppWidgetDataContent,
   onRefreshClick: () -> Unit
) {
   Column(
       modifier = GlanceModifier
           .fillMaxSize()
           .padding(12.dp)
           .background(GlanceTheme.colors.widgetBackground)
   ) {
       AppWidgetTitleContent(state.titleResId, false, onRefreshClick)
   }
}

@Composable
private fun AppWidgetTitleContent(
   @StringRes titleResId: Int,
   isLoading: Boolean,
   onRefreshClick: () -> Unit
) {
   val context = LocalContext.current
  
   Row(
       modifier = GlanceModifier.fillMaxWidth(),
       verticalAlignment = Alignment.CenterVertically
   ) {
       Text(
           text = context.getString(titleResId),
           modifier = GlanceModifier.defaultWeight(),
           maxLines = 1,
           style = TextStyle(
               fontSize = 16.sp,
               fontWeight = FontWeight.Medium,
               fontFamily = FontFamily.SansSerif,
               color = GlanceTheme.colors.onSurface
           )
       )
      
       when (isLoading) {
           true -> {
               CircularProgressIndicator(
                   modifier = GlanceModifier.size(24.dp),
                   color = GlanceTheme.colors.outline
               )
           }
           false -> {
               Image(
                   provider = ImageProvider(R.drawable.vec_refresh_primary),
                   contentDescription = null,
                   modifier = GlanceModifier.clickable { onRefreshClick() }
               )
           }
       }
   }
}

И добавил эту верстку в наш AppWidget в блок GlanceTheme. Важный нюанс верстки. Для отрисовки UI мы пользуемся библиотекой Glance. То есть импорты у нас следующие:

import androidx.glance.layout.Column
import androidx.glance.layout.Alignment
import androidx.glance.text.Text
...

Не перепутай.

Итог

Я показал как создать и добавить простой виджет для Android с использованием библиотеки Glance.

От автора

Это моя первая статья. Контента для нее оказалось слишком много и я решил разбить ее на несколько частей. Пока не знаю, сколько получится глав, но в планах показать следующее:

  • Добавление конфигурационной activity, в которой можно изменять настройки виджета;

  • Обновление виджета при помощи WorkManager. Например, у нас есть сетевой слой, там загружаются какие-то данные. Эти данные будут сохраняться в локальное хранилище, откуда виджет будет их доставать;

  • Взаимодействие с виджетом.

Возможно будут еще идеи и я их допишу. Часть кода со временем будет меняться, поэтому в итоговой статье я приложу ссылку на гитхаб.

Комментарии (2)


  1. Helpa
    02.09.2025 10:05

    @Klimov_ilya_sСпасибо за статью. А можно еще сделать репозиторий на github или где вам удобней, с проектом который описан в статье и видимо который будет продолжаться в следующих статьях.


  1. Spyman
    02.09.2025 10:05

    Я правильно понимаю - что Glance - не более чем обертка над remoteViews и функционально ничего не прибавляет?