Разработчик часто сталкивается с ситуацией: виджет добавлен на рабочий стол, но не обновляется, отображает пустую область или вовсе не появляется в списке доступных виджетов. Причина почти всегда кроется в ошибках конфигурации — неверно объявленный 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, и состояние между экземплярами не разделяется автоматически.
☑️ Чек-лист перед первым запуском виджета
⚠️ Внимание: виджеты не работают, если приложение установлено на 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 для длительных операций
Обновление данных и энергоэффективность
Виджет, показывающий устаревшие данные, бесполезен, но слишком частые обновления разряжают батарею и могут привести к ограничениям со стороны системы. Баланс достигается выбором правильного механизма под конкретную задачу.
| Способ обновления | Когда применять | Ограничения |
|---|---|---|
| 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 — эффект будет тем же.