Используйте виджеты коллекций

Попробуйте способ «Композиции»
Jetpack Compose — рекомендуемый набор инструментов для разработки пользовательского интерфейса для Android. Узнайте, как создавать виджеты с помощью API в стиле Compose.

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

Эти виджеты используют RemoteViewsService для отображения коллекций, основанных на удалённых данных, например, от поставщика контента . Виджет представляет данные с помощью одного из следующих типов представлений, известных как представления коллекций :

ListView
Представление, в котором элементы отображаются в виде вертикально прокручиваемого списка.
GridView
Представление, отображающее элементы в виде двухмерной прокручиваемой сетки.
StackView
Сложенный вид карточек — что-то вроде картотеки, — где пользователь может перелистнуть первую карточку вверх или вниз, чтобы увидеть предыдущую или следующую карточку соответственно.
AdapterViewFlipper
Простой ViewAnimator с поддержкой адаптера, который анимирует между двумя или более представлениями. Одновременно отображается только один дочерний элемент.

Поскольку эти представления-коллекции отображают коллекции, основанные на удалённых данных, они используют Adapter для привязки своего пользовательского интерфейса к данным. Adapter привязывает отдельные элементы из набора данных к отдельным объектам View .

А поскольку эти представления-коллекции поддерживаются адаптерами, фреймворк Android должен включать дополнительную архитектуру для поддержки их использования в виджетах. В контексте виджета Adapter заменяется RemoteViewsFactory — тонкой оберткой вокруг интерфейса Adapter . При запросе конкретного элемента коллекции RemoteViewsFactory создаёт и возвращает элемент коллекции в виде объекта RemoteViews . Чтобы включить представление-коллекцию в виджет, реализуйте RemoteViewsService и RemoteViewsFactory .

RemoteViewsService — это служба, позволяющая удалённому адаптеру запрашивать объекты RemoteViews . RemoteViewsFactory — это интерфейс для адаптера между представлением коллекции, таким как ListView , GridView и StackView , и базовыми данными для этого представления. Вот пример шаблонного кода для реализации этой службы и интерфейса из примера StackWidget :

Котлин

class StackWidgetService : RemoteViewsService() {

    override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
        return StackRemoteViewsFactory(this.applicationContext, intent)
    }
}

class StackRemoteViewsFactory(
        private val context: Context,
        intent: Intent
) : RemoteViewsService.RemoteViewsFactory {

// See the RemoteViewsFactory API reference for the full list of methods to
// implement.

}

Ява

public class StackWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {

// See the RemoteViewsFactory API reference for the full list of methods to
// implement.

}

Образец заявления

Фрагменты кода в этом разделе также взяты из примера StackWidget :

Рисунок 1. StackWidget .

Этот пример состоит из десяти представлений, отображающих значения от нуля до девяти. Этот пример виджета имеет следующие основные функции:

  • Пользователь может переместить верхнюю часть виджета вертикально, чтобы отобразить следующий или предыдущий вид. Это встроенное поведение StackView .

  • Виджет автоматически, без взаимодействия с пользователем, последовательно переключает свои представления, как в слайд-шоу. Это происходит благодаря настройке android:autoAdvanceViewId="@id/stack_view" в файле res/xml/stackwidgetinfo.xml . Эта настройка применяется к идентификатору представления, который в данном случае является идентификатором представления в стеке.

  • Если пользователь касается верхней панели, виджет отображает Toast сообщение «Touched view n », где n — индекс (позиция) затронутой панели. Подробнее о реализации поведений см. в разделе «Добавление поведения к отдельным элементам» .

Реализуйте виджеты с коллекциями

Чтобы реализовать виджет с коллекциями, следуйте процедуре реализации любого виджета , а затем выполните несколько дополнительных шагов: измените манифест, добавьте представление коллекции в макет виджета и измените подкласс AppWidgetProvider .

Манифест для виджетов с коллекциями

Помимо требований, перечисленных в разделе «Объявление виджета в манифесте» , необходимо обеспечить возможность привязки виджетов с коллекциями к вашему RemoteViewsService . Для этого необходимо объявить службу в файле манифеста с разрешением BIND_REMOTEVIEWS . Это предотвратит свободный доступ других приложений к данным вашего виджета.

Например, при создании виджета, который использует RemoteViewsService для заполнения представления коллекции, запись манифеста может выглядеть следующим образом:

<service android:name="MyWidgetService"
    android:permission="android.permission.BIND_REMOTEVIEWS" />

В этом примере android:name="MyWidgetService" ссылается на ваш подкласс RemoteViewsService .

Макет виджетов с коллекциями

Главное требование к XML-файлу макета виджета — он должен включать одно из представлений коллекции: ListView , GridView , StackView или AdapterViewFlipper . Вот файл widget_layout.xml для примера StackWidget :

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <StackView
        android:id="@+id/stack_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:loopViews="true" />
    <TextView
        android:id="@+id/empty_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:background="@drawable/widget_item_background"
        android:textColor="#ffffff"
        android:textStyle="bold"
        android:text="@string/empty_view_text"
        android:textSize="20sp" />
</FrameLayout>

Обратите внимание, что пустые представления должны быть родственными представлениям представления коллекции, для которого пустое представление представляет пустое состояние.

В дополнение к файлу макета для всего виджета создайте ещё один файл макета, который определяет макет каждого элемента в коллекции, например, макет каждой книги в коллекции. В примере StackWidget есть только один файл макета элемента, widget_item.xml , поскольку все элементы используют один и тот же макет.

Класс AppWidgetProvider для виджетов с коллекциями

Как и в случае с обычными виджетами, большая часть кода в подклассе AppWidgetProvider обычно находится в onUpdate() . Главное отличие в реализации onUpdate() при создании виджета с коллекциями заключается в необходимости вызова setRemoteAdapter() . Это сообщает представлению коллекции, откуда брать данные. После этого RemoteViewsService может вернуть вашу реализацию RemoteViewsFactory , и виджет сможет предоставить соответствующие данные. При вызове этого метода передайте намерение, указывающее на вашу реализацию RemoteViewsService , и идентификатор виджета, который нужно обновить.

Например, вот как пример StackWidget реализует метод обратного вызова onUpdate() для установки RemoteViewsService в качестве удаленного адаптера для коллекции виджетов:

Котлин

override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
) {
    // Update each of the widgets with the remote adapter.
    appWidgetIds.forEach { appWidgetId ->

        // Set up the intent that starts the StackViewService, which
        // provides the views for this collection.
        val intent = Intent(context, StackWidgetService::class.java).apply {
            // Add the widget ID to the intent extras.
            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
            data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
        }
        // Instantiate the RemoteViews object for the widget layout.
        val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
            // Set up the RemoteViews object to use a RemoteViews adapter.
            // This adapter connects to a RemoteViewsService through the
            // specified intent.
            // This is how you populate the data.
            setRemoteAdapter(R.id.stack_view, intent)

            // The empty view is displayed when the collection has no items.
            // It must be in the same layout used to instantiate the
            // RemoteViews object.
            setEmptyView(R.id.stack_view, R.id.empty_view)
        }

        // Do additional processing specific to this widget.

        appWidgetManager.updateAppWidget(appWidgetId, views)
    }
    super.onUpdate(context, appWidgetManager, appWidgetIds)
}

Ява

public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    // Update each of the widgets with the remote adapter.
    for (int i = 0; i < appWidgetIds.length; ++i) {

        // Set up the intent that starts the StackViewService, which
        // provides the views for this collection.
        Intent intent = new Intent(context, StackWidgetService.class);
        // Add the widget ID to the intent extras.
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
        intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
        // Instantiate the RemoteViews object for the widget layout.
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
        // Set up the RemoteViews object to use a RemoteViews adapter.
        // This adapter connects to a RemoteViewsService through the specified
        // intent.
        // This is how you populate the data.
        views.setRemoteAdapter(R.id.stack_view, intent);

        // The empty view is displayed when the collection has no items.
        // It must be in the same layout used to instantiate the RemoteViews
        // object.
        views.setEmptyView(R.id.stack_view, R.id.empty_view);

        // Do additional processing specific to this widget.

        appWidgetManager.updateAppWidget(appWidgetIds[i], views);
    }
    super.onUpdate(context, appWidgetManager, appWidgetIds);
}

Сохранение данных

Как описано на этой странице, подкласс RemoteViewsService предоставляет RemoteViewsFactory , используемый для заполнения представления удаленной коллекции.

В частности, выполните следующие действия:

  1. Подкласс RemoteViewsService . RemoteViewsService — это служба, через которую удаленный адаптер может запрашивать RemoteViews .

  2. В подклассе RemoteViewsService включите класс, реализующий интерфейс RemoteViewsFactory . RemoteViewsFactory — это интерфейс для адаптера между удалённым представлением коллекции, таким как ListView , GridView , StackView , и базовыми данными для этого представления. Ваша реализация отвечает за создание объекта RemoteViews для каждого элемента в наборе данных. Этот интерфейс представляет собой тонкую оболочку вокруг Adapter .

Вы не можете полагаться на сохранение одного экземпляра вашей службы или любых содержащихся в ней данных. Не храните данные в RemoteViewsService , если они не являются статическими. Если вы хотите, чтобы данные вашего виджета сохранялись, лучшим решением будет использовать ContentProvider , данные которого сохраняются и после окончания жизненного цикла процесса. Например, виджет продуктового магазина может хранить состояние каждого элемента списка покупок в постоянном месте, например, в базе данных SQL.

Основным содержимым реализации RemoteViewsService является RemoteViewsFactory , описанный в следующем разделе.

Интерфейс RemoteViewsFactory

Ваш пользовательский класс, реализующий интерфейс RemoteViewsFactory , предоставляет виджету данные об элементах его коллекции. Для этого он объединяет XML-файл макета элементов виджета с источником данных. Этот источник данных может быть чем угодно: от базы данных до простого массива. В примере StackWidget источником данных является массив WidgetItems . RemoteViewsFactory выступает в качестве адаптера для присоединения данных к представлению удалённой коллекции.

Два наиболее важных метода, которые необходимо реализовать для подкласса RemoteViewsFactory , — это onCreate() и getViewAt() .

Система вызывает onCreate() при первом создании фабрики. Здесь вы настраиваете все подключения или курсоры к источнику данных. Например, в примере StackWidget метод onCreate() используется для инициализации массива объектов WidgetItem . Когда виджет активен, система обращается к этим объектам, используя их индекс в массиве, и отображает содержащийся в них текст.

Вот отрывок из реализации RemoteViewsFactory примера StackWidget , который показывает части метода onCreate() :

Котлин

private const val REMOTE_VIEW_COUNT: Int = 10

class StackRemoteViewsFactory(
        private val context: Context
) : RemoteViewsService.RemoteViewsFactory {

    private lateinit var widgetItems: List<WidgetItem>

    override fun onCreate() {
        // In onCreate(), set up any connections or cursors to your data
        // source. Heavy lifting, such as downloading or creating content,
        // must be deferred to onDataSetChanged() or getViewAt(). Taking
        // more than 20 seconds on this call results in an ANR.
        widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") }
        ...
    }
    ...
}

Ява

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
    private static final int REMOTE_VIEW_COUNT = 10;
    private List<WidgetItem> widgetItems = new ArrayList<WidgetItem>();

    public void onCreate() {
        // In onCreate(), setup any connections or cursors to your data
        // source. Heavy lifting, such as downloading or creating content,
        // must be deferred to onDataSetChanged() or getViewAt(). Taking
        // more than 20 seconds on this call results in an ANR.
        for (int i = 0; i < REMOTE_VIEW_COUNT; i++) {
            widgetItems.add(new WidgetItem(i + "!"));
        }
        ...
    }
...

Метод RemoteViewsFactory getViewAt() возвращает объект RemoteViews , соответствующий данным в указанной position в наборе данных. Вот фрагмент реализации RemoteViewsFactory в примере StackWidget :

Котлин

override fun getViewAt(position: Int): RemoteViews {
    // Construct a remote views item based on the widget item XML file
    // and set the text based on the position.
    return RemoteViews(context.packageName, R.layout.widget_item).apply {
        setTextViewText(R.id.widget_item, widgetItems[position].text)
    }
}

Ява

public RemoteViews getViewAt(int position) {
    // Construct a remote views item based on the widget item XML file
    // and set the text based on the position.
    RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_item);
    views.setTextViewText(R.id.widget_item, widgetItems.get(position).text);
    return views;
}

Добавить поведение к отдельным элементам

В предыдущих разделах показано, как привязать данные к коллекции виджетов. Но что делать, если вы хотите добавить динамическое поведение к отдельным элементам в представлении коллекции?

Как описано в разделе Обработка событий с помощью класса onUpdate() , метод setOnClickPendingIntent() обычно используется для настройки поведения объекта при нажатии, например, для запуска Activity при нажатии кнопки. Однако такой подход недопустим для дочерних представлений в отдельных элементах коллекции. Например, setOnClickPendingIntent() можно использовать для настройки глобальной кнопки в виджете Gmail, которая запускает, например, само приложение, но не отдельные элементы списка.

Вместо этого, чтобы добавить поведение щелчка к отдельным элементам коллекции, используйте setOnClickFillInIntent() . Это подразумевает настройку шаблона отложенного намерения для представления коллекции, а затем установку намерения заполнения для каждого элемента коллекции через RemoteViewsFactory .

В этом разделе пример StackWidget описывает, как добавить поведение к отдельным элементам. В примере StackWidget , если пользователь касается верхней панели, виджет отображает Toast сообщение «Touched view n », где n — индекс (позиция) затронутой панели. Вот как это работает:

  • StackWidgetProvider — подкласс AppWidgetProvider — создает отложенное намерение с пользовательским действием, называемым TOAST_ACTION .

  • Когда пользователь касается представления, срабатывает намерение и передается TOAST_ACTION .

  • Эта трансляция перехватывается методом onReceive() класса StackWidgetProvider , и виджет отображает Toast сообщение для выбранного представления. Данные об элементах коллекции предоставляются RemoteViewsFactory через RemoteViewsService .

Настройте шаблон ожидающего намерения

StackWidgetProvider (подкласс AppWidgetProvider ) устанавливает ожидающее намерение. Отдельные элементы коллекции не могут устанавливать собственные ожидающие намерения. Вместо этого коллекция в целом устанавливает шаблон ожидающего намерения, а отдельные элементы устанавливают заполняющее намерение для создания уникального поведения для каждого элемента.

Этот класс также получает широковещательное сообщение, отправляемое при касании пользователем представления. Он обрабатывает это событие в методе onReceive() . Если действие намерения — TOAST_ACTION , виджет отображает Toast сообщение для текущего представления.

Котлин

const val TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION"
const val EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM"

class StackWidgetProvider : AppWidgetProvider() {

    ...

    // Called when the BroadcastReceiver receives an Intent broadcast.
    // Checks whether the intent's action is TOAST_ACTION. If it is, the
    // widget displays a Toast message for the current item.
    override fun onReceive(context: Context, intent: Intent) {
        val mgr: AppWidgetManager = AppWidgetManager.getInstance(context)
        if (intent.action == TOAST_ACTION) {
            val appWidgetId: Int = intent.getIntExtra(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID
            )
            // EXTRA_ITEM represents a custom value provided by the Intent
            // passed to the setOnClickFillInIntent() method to indicate the
            // position of the clicked item. See StackRemoteViewsFactory in
            // Set the fill-in Intent for details.
            val viewIndex: Int = intent.getIntExtra(EXTRA_ITEM, 0)
            Toast.makeText(context, "Touched view $viewIndex", Toast.LENGTH_SHORT).show()
        }
        super.onReceive(context, intent)
    }

    override fun onUpdate(
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetIds: IntArray
    ) {
        // Update each of the widgets with the remote adapter.
        appWidgetIds.forEach { appWidgetId ->

            // Sets up the intent that points to the StackViewService that
            // provides the views for this collection.
            val intent = Intent(context, StackWidgetService::class.java).apply {
                putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                // When intents are compared, the extras are ignored, so embed
                // the extra sinto the data so that the extras are not ignored.
                data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
            }
            val rv = RemoteViews(context.packageName, R.layout.widget_layout).apply {
                setRemoteAdapter(R.id.stack_view, intent)

                // The empty view is displayed when the collection has no items.
                // It must be a sibling of the collection view.
                setEmptyView(R.id.stack_view, R.id.empty_view)
            }

            // This section makes it possible for items to have individualized
            // behavior. It does this by setting up a pending intent template.
            // Individuals items of a collection can't set up their own pending
            // intents. Instead, the collection as a whole sets up a pending
            // intent template, and the individual items set a fillInIntent
            // to create unique behavior on an item-by-item basis.
            val toastPendingIntent: PendingIntent = Intent(
                    context,
                    StackWidgetProvider::class.java
            ).run {
                // Set the action for the intent.
                // When the user touches a particular view, it has the effect of
                // broadcasting TOAST_ACTION.
                action = TOAST_ACTION
                putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))

                PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT)
            }
            rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent)

            appWidgetManager.updateAppWidget(appWidgetId, rv)
        }
        super.onUpdate(context, appWidgetManager, appWidgetIds)
    }
}

Ява

public class StackWidgetProvider extends AppWidgetProvider {
    public static final String TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION";
    public static final String EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM";

    ...

    // Called when the BroadcastReceiver receives an Intent broadcast.
    // Checks whether the intent's action is TOAST_ACTION. If it is, the
    // widget displays a Toast message for the current item.
    @Override
    public void onReceive(Context context, Intent intent) {
        AppWidgetManager mgr = AppWidgetManager.getInstance(context);
        if (intent.getAction().equals(TOAST_ACTION)) {
            int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
            // EXTRA_ITEM represents a custom value provided by the Intent
            // passed to the setOnClickFillInIntent() method to indicate the
            // position of the clicked item. See StackRemoteViewsFactory in
            // Set the fill-in Intent for details.
            int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
            Toast.makeText(context, "Touched view " + viewIndex, Toast.LENGTH_SHORT).show();
        }
        super.onReceive(context, intent);
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // Update each of the widgets with the remote adapter.
        for (int i = 0; i < appWidgetIds.length; ++i) {

            // Sets up the intent that points to the StackViewService that
            // provides the views for this collection.
            Intent intent = new Intent(context, StackWidgetService.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            // When intents are compared, the extras are ignored, so embed
            // the extras into the data so that the extras are not
            // ignored.
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
            rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);

            // The empty view is displayed when the collection has no items. It
            // must be a sibling of the collection view.
            rv.setEmptyView(R.id.stack_view, R.id.empty_view);

            // This section makes it possible for items to have individualized
            // behavior. It does this by setting up a pending intent template.
            // Individuals items of a collection can't set up their own pending
            // intents. Instead, the collection as a whole sets up a pending
            // intent template, and the individual items set a fillInIntent
            // to create unique behavior on an item-by-item basis.
            Intent toastIntent = new Intent(context, StackWidgetProvider.class);
            // Set the action for the intent.
            // When the user touches a particular view, it has the effect of
            // broadcasting TOAST_ACTION.
            toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);
            toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
            rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);

            appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
        }
        super.onUpdate(context, appWidgetManager, appWidgetIds);
    }
}

Установите цель заполнения

Ваш RemoteViewsFactory должен задать намерение заполнения для каждого элемента коллекции. Это позволяет различать отдельные действия по щелчку для каждого элемента. Затем намерение заполнения объединяется с шаблоном PendingIntent для определения конечного намерения, которое выполняется при касании элемента.

Котлин

private const val REMOTE_VIEW_COUNT: Int = 10

class StackRemoteViewsFactory(
        private val context: Context,
        intent: Intent
) : RemoteViewsService.RemoteViewsFactory {

    private lateinit var widgetItems: List<WidgetItem>
    private val appWidgetId: Int = intent.getIntExtra(
            AppWidgetManager.EXTRA_APPWIDGET_ID,
            AppWidgetManager.INVALID_APPWIDGET_ID
    )

    override fun onCreate() {
        // In onCreate(), set up any connections or cursors to your data source.
        // Heavy lifting, such as downloading or creating content, must be
        // deferred to onDataSetChanged() or getViewAt(). Taking more than 20
        // seconds on this call results in an ANR.
        widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") }
        ...
    }
    ...

    override fun getViewAt(position: Int): RemoteViews {
        // Construct a remote views item based on the widget item XML file
        // and set the text based on the position.
        return RemoteViews(context.packageName, R.layout.widget_item).apply {
            setTextViewText(R.id.widget_item, widgetItems[position].text)

            // Set a fill-intent to fill in the pending intent template.
            // that is set on the collection view in StackWidgetProvider.
            val fillInIntent = Intent().apply {
                Bundle().also { extras ->
                    extras.putInt(EXTRA_ITEM, position)
                    putExtras(extras)
                }
            }
            // Make it possible to distinguish the individual on-click
            // action of a given item.
            setOnClickFillInIntent(R.id.widget_item, fillInIntent)
            ...
        }
    }
    ...
}

Ява

public class StackWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
    private static final int count = 10;
    private List<WidgetItem> widgetItems = new ArrayList<WidgetItem>();
    private Context context;
    private int appWidgetId;

    public StackRemoteViewsFactory(Context context, Intent intent) {
        this.context = context;
        appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
    }

    // Initialize the data set.
    public void onCreate() {
        // In onCreate(), set up any connections or cursors to your data
        // source. Heavy lifting, such as downloading or creating
        // content, must be deferred to onDataSetChanged() or
        // getViewAt(). Taking more than 20 seconds on this call results
        // in an ANR.
        for (int i = 0; i < count; i++) {
            widgetItems.add(new WidgetItem(i + "!"));
        }
        ...
    }

    // Given the position (index) of a WidgetItem in the array, use the
    // item's text value in combination with the widget item XML file to
    // construct a RemoteViews object.
    public RemoteViews getViewAt(int position) {
        // Position always ranges from 0 to getCount() - 1.

        // Construct a RemoteViews item based on the widget item XML
        // file and set the text based on the position.
        RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_item);
        rv.setTextViewText(R.id.widget_item, widgetItems.get(position).text);

        // Set a fill-intent to fill in the pending
        // intent template that is set on the collection view in
        // StackWidgetProvider.
        Bundle extras = new Bundle();
        extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);
        Intent fillInIntent = new Intent();
        fillInIntent.putExtras(extras);
        // Make it possible to distinguish the individual on-click
        // action of a given item.
        rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);

        // Return the RemoteViews object.
        return rv;
    }
    ...
}

Поддерживайте актуальность собираемых данных

На рисунке 2 показан процесс обновления виджета, использующего коллекции. Он показывает, как код виджета взаимодействует с RemoteViewsFactory и как можно инициировать обновления:

Рисунок 2. Взаимодействие с RemoteViewsFactory во время обновлений.

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

Для этого используйте AppWidgetManager для вызова метода notifyAppWidgetViewDataChanged() . Этот вызов приводит к обратному вызову метода onDataSetChanged() объекта RemoteViewsFactory , который позволяет получить любые новые данные.

Вы можете синхронно выполнять ресурсоёмкие операции в обратном вызове onDataSetChanged() . Вы гарантированно завершите этот вызов до того, как метаданные или данные представления будут извлечены из RemoteViewsFactory . Вы также можете выполнять ресурсоёмкие операции в методе getViewAt() . Если этот вызов занимает много времени, представление загрузки, указанное методом getLoadingView() объекта RemoteViewsFactory , отображается в соответствующей позиции представления коллекции до тех пор, пока не будет выполнен возврат.

Используйте RemoteCollectionItems для прямой передачи коллекции

В Android 12 (API уровня 31) добавлен метод setRemoteAdapter(int viewId, RemoteViews.RemoteCollectionItems items) , который позволяет приложению передавать коллекцию напрямую при заполнении представления коллекции. Если вы настраиваете адаптер с помощью этого метода, вам не нужно реализовывать RemoteViewsFactory и вызывать notifyAppWidgetViewDataChanged() .

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

Если коллекция не использует постоянный набор макетов (то есть некоторые элементы присутствуют лишь иногда), используйте setViewTypeCount , чтобы указать максимальное количество уникальных макетов, которые может содержать коллекция. Это позволит повторно использовать адаптер при обновлениях виджета вашего приложения.

Вот пример реализации упрощенных коллекций RemoteViews .

Котлин

val itemLayouts = listOf(
        R.layout.item_type_1,
        R.layout.item_type_2,
        ...
)

remoteView.setRemoteAdapter(
        R.id.list_view,
        RemoteViews.RemoteCollectionItems.Builder()
            .addItem(/* id= */ ID_1, RemoteViews(context.packageName, R.layout.item_type_1))
            .addItem(/* id= */ ID_2, RemoteViews(context.packageName, R.layout.item_type_2))
            ...
            .setViewTypeCount(itemLayouts.count())
            .build()
)

Ява

List<Integer> itemLayouts = Arrays.asList(
    R.layout.item_type_1,
    R.layout.item_type_2,
    ...
);

remoteView.setRemoteAdapter(
    R.id.list_view,
    new RemoteViews.RemoteCollectionItems.Builder()
        .addItem(/* id= */ ID_1, new RemoteViews(context.getPackageName(), R.layout.item_type_1))
        .addItem(/* id= */ ID_2, new RemoteViews(context.getPackageName(), R.layout.item_type_2))
        ...
        .setViewTypeCount(itemLayouts.size())
        .build()
);