Как сделать виджет на Android: полное руководство

Создание виджета для Android начинается не с кода, а с понимания архитектуры: виджет — это не самостоятельное приложение, а компонент AppWidget, который работает через RemoteViews и обновляется системой по расписанию или по событию. Если виджет не появляется в списке после установки приложения — почти всегда причина в неправильно объявленном ресивере в манифесте или отсутствии файла метаданных appwidget-provider.

В этом руководстве разберём полный цикл: от создания проекта в Android Studio до обновления данных на виджете и обработки нажатий. Материал ориентирован на разработчиков, знакомых с основами Kotlin и структурой Android-проекта.

Что такое виджет и как он работает

Виджет рабочего стола (App Widget) — это миниатюрный интерфейс приложения, размещаемый на лаунчере. Ключевое ограничение: виджет не может использовать произвольные View. Доступен только ограниченный набор компонентов, поддерживаемых RemoteViews: TextView, ImageView, ProgressBar, ListView и несколько других.

Система управляет виджетом через три связанных элемента:

  • 🧩 AppWidgetProvider — BroadcastReceiver, получающий события жизненного цикла виджета (создание, обновление, удаление);
  • 📄 AppWidgetProviderInfo — XML-файл с метаданными: размер, период обновления, макет;
  • 🎨 Layout — XML-разметка внешнего вида, загружаемая через RemoteViews.

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

Подготовка проекта в Android Studio

Откройте существующий проект или создайте новый через File → New → New Project. Отдельного шаблона для виджета не требуется — он добавляется в любое приложение. Самый быстрый способ сгенерировать каркас: File → New → Widget → App Widget. Мастер создаст класс провайдера, XML-метаданные, макет и пропишет ресивер в манифесте.

Если добавляете виджет вручную, проверьте, что в AndroidManifest.xml ресивер объявлен с правильным intent-фильтром:

<receiver android:name=".MyWidgetProvider"

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>

⚠️ Внимание: если в манифесте отсутствует блок meta-data со ссылкой на XML-файл провайдера, виджет не отобразится в системном списке виджетов — это самая частая причина «исчезнувшего» виджета у начинающих разработчиков.

Создание файла метаданных виджета

Файл res/xml/my_widget_info.xml описывает параметры виджета для системы. Без него лаунчер не узнает, какого размера виджет и как часто его обновлять.

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"

android:minWidth="180dp"

android:minHeight="110dp"

android:updatePeriodMillis="1800000"

android:initialLayout="@layout/widget_layout"

android:resizeMode="horizontal|vertical"

android:widgetCategory="home_screen" />

Обратите внимание на параметр updatePeriodMillis: система не гарантирует точное срабатывание и может объединять обновления для экономии заряда. Для данных, требующих частого обновления (часы, погода в реальном времени), используйте WorkManager или обновление по событию, а не полагайтесь только на периодический таймер.

Вёрстка макета виджета

Макет виджета — обычный XML-файл в res/layout, но с ограничением по поддерживаемым элементам. Разрешены FrameLayout, LinearLayout, RelativeLayout, GridLayout в качестве контейнеров и базовые виджеты вроде TextView, ImageView, Button, ProgressBar.

Пример простого макета с текстом и кнопкой:

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

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical"

android:padding="12dp">

<TextView

android:id="@+id/widget_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Загрузка..." />

<Button

android:id="@+id/widget_button"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Обновить" />

</LinearLayout>

Фон виджета лучше делать с закруглёнными углами через drawable-ресурс: на современных версиях Android система сама обрезает углы, но явный фон даёт предсказуемый результат на разных лаунчерах.

Реализация AppWidgetProvider

Класс провайдера наследуется от AppWidgetProvider и переопределяет колбэки жизненного цикла. Главный из них — onUpdate(), вызываемый при добавлении виджета и при плановых обновлениях.

class MyWidgetProvider : 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, "Данные обновлены")

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)

appWidgetManager.updateAppWidget(widgetId, views)

}

}

}

Обратите внимание на флаг PendingIntent.FLAG_IMMUTABLE — на новых версиях Android его явное указание обязательно, иначе приложение упадёт с исключением при создании PendingIntent.

📊 Что для вас самое сложное в разработке виджета?
Обновление данных в фоне
Обработка нажатий через PendingIntent
Вёрстка в рамках RemoteViews
Настройка манифеста и метаданных

Обновление данных и обработка нажатий

Для фонового обновления данных (например, загрузки из сети) используйте WorkManager — это рекомендуемый системой способ отложенных задач. Прямые сетевые запросы из onUpdate() недопустимы: метод выполняется в главном потоке BroadcastReceiver, и длительная операция приведёт к ANR.

Порядок действий для обновления из WorkManager:

  • ⚙️ Создайте Worker, который загружает данные и сохраняет их (например, в SharedPreferences или Room);
  • 🔄 В конце работы воркера вызовите AppWidgetManager.notifyAppWidgetViewDataChanged() или updateAppWidget();
  • 📅 Запланируйте периодическую задачу через PeriodicWorkRequest с разумным интервалом.
⚠️ Внимание: не храните состояние виджета в статических полях провайдера. Процесс приложения может быть уничтожен системой в любой момент — данные нужно сохранять в постоянное хранилище.

☑️ Проверка перед публикацией виджета

Выполнено: 0 / 6

Тестирование и типичные ошибки

Тестируйте виджет на реальном устройстве, а не только на эмуляторе: сторонние лаунчеры (Nova Launcher, фирменные оболочки Samsung, Xiaomi) могут по-разному обрабатывать размеры и обновления. Установите приложение, долгим нажатием на рабочем столе откройте список виджетов и перетащите свой на экран.

ПроблемаВероятная причинаРешение
Виджет не отображается в спискеНет meta-data в манифесте или неверный intent-filterПроверить объявление ресивера
Виджет не обновляетсяСлишком малый updatePeriodMillis игнорируетсяИспользовать WorkManager
Кнопка не реагирует на нажатиеPendingIntent создан без FLAG_IMMUTABLEДобавить обязательный флаг
Пустая область вместо виджетаНеподдерживаемый View в макетеОставить только RemoteViews-совместимые элементы
Падение при добавлении виджетаОшибка в onUpdate() или неверный ID ресурсаСмотреть стектрейс в Logcat

Отладку удобно вести через Logcat с фильтром по тегу приложения: все исключения в провайдере попадают в системный лог, даже если само приложение не запущено в foreground.

Как сделать виджет со списком (ListView)

Для коллекций используется RemoteViewsService и RemoteViewsFactory — аналог адаптера. Объявите сервис в манифесте с разрешением BIND_REMOTEVIEWS, в макете добавьте ListView, а в провайдере привяжите его через setRemoteAdapter(). Для обновления данных списка вызывайте notifyAppWidgetViewDataChanged().

Полезные рекомендации

Виджет должен быть лёгким и полезным с первого взгляда. Не перегружайте его элементами: пользователь рассчитывает получить ключевую информацию за секунду. Если данных много — показывайте сводку и открывайте полное приложение по нажатию.

Учитывайте тёмную тему: цвета фона и текста должны читаться в обоих режимах. Используйте ресурсы из values-night или системные атрибуты цвета. Также проверьте виджет при разных размерах ячеек — пользователь может растянуть или сжать его, если включён resizeMode.

Частые вопросы

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

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

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

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

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

Через configuration Activity: укажите её в XML-провайдере атрибутом android:configure. Активность откроется при добавлении виджета и должна вернуть результат с ID виджета, иначе виджет не будет добавлен.

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

Да, через библиотеку Glance от Jetpack — она позволяет описывать виджеты декларативно в стиле Compose, компилируя результат в RemoteViews. Это современный подход, но требует подключения отдельной зависимости и изучения API Glance.

Какой минимальный размер виджета?

Размер задаётся в ячейках сетки лаунчера через minWidth и minHeight в dp. Ориентировочно одна ячейка соответствует примерно 70dp, но точный пересчёт зависит от устройства — ориентируйтесь на рекомендации официальной документации Android и тестируйте на реальных лаунчерах.