Создание виджетов для Android: полное руководство

Разработчик часто сталкивается с ситуацией: виджет добавлен на рабочий стол, но не обновляется, отображает пустую область или вовсе не появляется в списке доступных виджетов. Причина почти всегда кроется в ошибках конфигурации — неверно объявленный AppWidgetProvider в манифесте, отсутствующий файл appwidget-provider в ресурсах или неправильно настроенный BroadcastReceiver. Прежде чем искать баг в коде, проверьте именно эти три точки — они отвечают за регистрацию виджета в системе.

Виджет рабочего стола (App Widget) — это миниатюрное представление приложения, которое пользователь размещает на домашнем экране. Оно позволяет получать информацию и выполнять простые действия без запуска самого приложения: посмотреть погоду, запустить таймер, пролистать список задач. В этой статье разберём полный цикл создания виджета для Android: от структуры проекта до обновления данных и публикации.

Как устроены виджеты в Android

Виджет — это не самостоятельное приложение, а компонент, который существует внутри вашего APK или AAB-пакета. Система Android отрисовывает его через механизм RemoteViews — ограниченный набор представлений, который может быть сериализован и передан другому процессу (лаунчеру). Именно поэтому в виджетах нельзя использовать произвольные кастомные View: доступны только стандартные элементы вроде TextView, ImageView, ProgressBar, ListView и некоторых других.

Архитектура виджета строится на трёх китах. Первый — класс-провайдер, наследник AppWidgetProvider, который получает системные события жизненного цикла. Второй — XML-файл метаданных в папке res/xml, описывающий размеры, период обновления и превью. Третий — layout-файл с разметкой самого виджета.

  • 📱 AppWidgetProvider — BroadcastReceiver, обрабатывающий события onUpdate, onEnabled, onDisabled и onDeleted
  • 🖼️ RemoteViews — механизм отрисовки интерфейса виджета в чужом процессе (лаунчере)
  • ⚙️ AppWidgetManager — системный сервис для обновления и управления экземплярами виджетов
  • 📄 XML-метаданные — описание поведения виджета: размеры, ресайз, превью, категория

Подготовка проекта и объявление виджета в манифесте

Создайте новый проект в Android Studio или откройте существующий. Первым делом объявите провайдер в файле AndroidManifest.xml внутри тега application. Без корректной записи система просто не увидит ваш виджет — это самая частая причина «невидимости» в списке лаунчера.

<receiver

android:name=".MyAppWidgetProvider"

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/my_widget_info" />

</receiver>

Обратите внимание на атрибут android:exported. Начиная с Android 12 (API 31) его явное указание обязательно для компонентов с intent-filter. Для виджета, который обновляется только системой, значение false обычно достаточно — системные broadcast-сообщения доставляются и так.

⚠️ Внимание: если после установки приложения виджет не появляется в списке лаунчера, первым делом проверьте, что имя пакета и класса в манифесте совпадают с реальным расположением файла провайдера, а action указан точно как android.appwidget.action.APPWIDGET_UPDATE — опечатка в одной букве полностью отключает регистрацию.

Файл метаданных appwidget-provider

Создайте в папке res/xml файл, например my_widget_info.xml. Он описывает характеристики виджета: минимальные размеры в ячейках, возможность изменения размера, период автообновления и изображение-превью для пикера виджетов.

<appwidget-provider

xmlns:android="http://schemas.android.com/apk/res/android"

android:minWidth="110dp"

android:minHeight="110dp"

android:updatePeriodMillis="1800000"

android:initialLayout="@layout/widget_layout"

android:resizeMode="horizontal|vertical"

android:widgetCategory="home_screen"

android:previewImage="@drawable/widget_preview" />

Система рассчитывает занимаемые ячейки по формуле: размер в dp делится на размер ячейки с учётом отступов. Практическое правило — число ячеек примерно равно (размер в dp − 30) / 70, но точное поведение зависит от лаунчера, поэтому тестируйте виджет на разных домашних экранах.

Атрибут updatePeriodMillis задаёт желаемый интервал обновления, однако система может игнорировать слишком частые запросы для экономии батареи — значения меньше 30 минут не гарантированы. Если нужны точные по времени обновления, используйте WorkManager или AlarmManager вместо этого атрибута.

Создание макета и класса AppWidgetProvider

Layout виджета создаётся в res/layout как обычный XML-макет, но с ограничением: только разрешённые для RemoteViews компоненты. Поддерживаются LinearLayout, FrameLayout, RelativeLayout, GridLayout, а из виджетов — TextView, ImageView, Button, ProgressBar, Chronometer, адаптерные списки и некоторые другие.

Класс провайдера наследуется от AppWidgetProvider и переопределяет метод onUpdate, который система вызывает при добавлении виджета и по таймеру обновления:

class MyAppWidgetProvider : AppWidgetProvider() {

override fun onUpdate(

context: Context,

appWidgetManager: AppWidgetManager,

appWidgetIds: IntArray

) {

for (widgetId in appWidgetIds) {

val views = RemoteViews(context.packageName, R.layout.widget_layout)

views.setTextViewText(R.id.widget_text, "Обновлено")

appWidgetManager.updateAppWidget(widgetId, views)

}

}

}

Вам нужно помнить: onUpdate получает массив ID, потому что пользователь может разместить несколько копий одного виджета. Каждая копия обновляется отдельно через свой appWidgetId, и состояние между экземплярами не разделяется автоматически.

☑️ Чек-лист перед первым запуском виджета

Выполнено: 0 / 5
⚠️ Внимание: виджеты не работают, если приложение установлено на SD-карту — после отключения накопителя система теряет к ним доступ. По возможности не разрешайте установку на внешний носитель через android:installLocation, если виджет — ключевая функция приложения.

Обработка нажатий и интерактивность

Клики по элементам виджета обрабатываются через PendingIntent, привязанный к конкретному View. Поскольку виджет живёт в процессе лаунчера, прямые OnClickListener недоступны — вместо этого вы регистрируете интент, который система выполнит от имени вашего приложения.

val intent = Intent(context, MainActivity::class.java)

val pendingIntent = PendingIntent.getActivity(

context, 0, intent,

PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

)

views.setOnClickPendingIntent(R.id.widget_button, pendingIntent)

Флаг FLAG_IMMUTABLE обязателен на современных версиях Android: без явного указания мутабельности приложение упадёт с исключением на Android 12 и новее. Для кнопок, которые должны обновлять сам виджет без открытия Activity, отправьте broadcast на ваш провайдер и обработайте его в onReceive.

  • 👆 Открытие Activity — PendingIntent.getActivity для перехода в приложение
  • 🔄 Обновление виджета — PendingIntent.getBroadcast с кастомным action
  • ⚡ Фоновая задача — PendingIntent.getForegroundService для длительных операций
📊 Что было самым сложным при создании вашего первого виджета?
Настройка манифеста и метаданных
Работа с RemoteViews
Обновление данных в фоне
Обработка нажатий через PendingIntent

Обновление данных и энергоэффективность

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

Способ обновленияКогда применятьОграничения
updatePeriodMillisРедкие фоновые обновленияНе чаще ~30 минут, неточный тайминг
WorkManagerПериодическая загрузка данных из сетиМинимальный интервал около 15 минут
AlarmManagerТочное время обновления (часы, будильник)Требует аккуратности с doze-режимом
Обновление по событиюРеакция на действие пользователя или pushНужен триггер из приложения или FCM

Для виджетов со списками (например, лента задач или почта) используйте RemoteViewsService в связке с RemoteViewsFactory — это аналог RecyclerView-адаптера для виджетов. Система сама подгружает элементы по мере прокрутки, что снижает потребление памяти лаунчером.

Почему виджет перестаёт обновляться после «убийства» приложения

На многих прошивках (особенно с агрессивной оптимизацией батареи) система блокирует фоновые процессы приложений, которых нет в белом списке. Проверьте настройки батареи для приложения, добавьте его в исключения оптимизации и используйте WorkManager — он корректнее переживает перезапуски, чем самописные сервисы.

Тестирование и публикация

Тестировать виджет только на эмуляторе недостаточно: поведение лаунчеров различается. Проверьте добавление, изменение размера, удаление и повторное добавление виджета минимум на стандартном лаунчере и одном-двух популярных сторонних. Особое внимание уделите сценарию, когда у виджета несколько экземпляров с разным содержимым.

Полезно логировать вызовы onUpdate, onEnabled и onDeleted через Log.d — так вы увидите реальную частоту системных обновлений и поймёте, срабатывает ли ваш таймер. Перед релизом в Google Play убедитесь, что превью-изображение актуально, а виджет корректно выглядит в тёмной теме — многие пользователи держат её включённой постоянно.

Частые вопросы о создании виджетов для Android

Почему мой виджет не отображается в списке виджетов?

Чаще всего причина в ошибках манифеста: неверное имя класса, опечатка в action APPWIDGET_UPDATE или отсутствие meta-data со ссылкой на XML-файл провайдера. Также проверьте, что приложение установлено во внутреннюю память, а само приложение хотя бы раз запускалось после установки.

Можно ли использовать Jetpack Compose для виджетов?

Да, существует библиотека Glance от Google, которая позволяет описывать виджеты в декларативном стиле, похожем на Compose. Под капотом она всё равно генерирует RemoteViews, поэтому системные ограничения сохраняются, но разработка заметно упрощается.

Как сделать виджет с настройками перед добавлением?

Создайте конфигурационную Activity и укажите её в атрибуте android:configure файла appwidget-provider. Система откроет её при добавлении виджета. Важно: Activity должна вернуть результат RESULT_OK с ID виджета, иначе добавление будет отменено.

Почему updatePeriodMillis меньше 30 минут не работает?

Это ограничение системы для экономии заряда батареи: слишком частые пробуждения устройства заблокированы на уровне ОС. Для более частых обновлений используйте WorkManager, события из приложения или push-уведомления через FCM.

Как обновить виджет из кода приложения вручную?

Получите экземпляр AppWidgetManager, затем получите ID всех экземпляров вашего виджета через getAppWidgetIds с ComponentName вашего провайдера и вызовите updateAppWidget с новым RemoteViews. Либо отправьте broadcast с action APPWIDGET_UPDATE — эффект будет тем же.