Use widget collections

Collection widgets specialize in displaying many elements of the same type, such as a collection of pictures from a gallery app, a collection of articles from a news app or a collection of messages from a communication app. Collection widgets typically focus on two use cases: browsing the collection, and opening an element of the collection to its detail view for consumption. Collection widgets can scroll vertically.

These kinds of widgets use the RemoteViewsService to display collections that are backed by remote data, such as from a content provider. The data provided by the presented in the widget using one of the following view types, which we’ll refer to as collection views:

ListView
A view that shows items in a vertically scrolling list.
GridView
A view that shows items in two-dimensional scrolling grid.
StackView
A stacked card view (kind of like a rolodex), where the user can flick the front card up/down to see the previous/next card, respectively.
AdapterViewFlipper
An adapter-backed simple ViewAnimator that animates between two or more views. Only one child is shown at a time.

As stated previously in this document, these collection views display collections backed by remote data. This means that they use an Adapter to bind their user interface to their data. An Adapter binds individual items from a set of data into individual View objects. Because these collection views are backed by adapters, the Android framework must include extra architecture to support their use in widgets. In the context of a widget, the Adapter is replaced by a RemoteViewsFactory, which is simply a thin wrapper around the Adapter interface. When requested for a specific item in the collection, the RemoteViewsFactory creates and returns the item for the collection as a RemoteViews object. In order to include a collection view in your widget, you must implement RemoteViewsService and RemoteViewsFactory.

RemoteViewsService is a service that allows a remote adapter to request RemoteViews objects. RemoteViewsFactory is an interface for an adapter between a collection view (such as ListView, GridView, and StackView) and the underlying data for that view. From the StackWidget sample, here is an example of the boilerplate code you use to implement this service and interface:

Kotlin

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.

}

Java

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.

}

Sample application

The code excerpts in this section are drawn from the StackWidget sample:

This sample consists of a stack of 10 views, which display the values "0" through "9" The sample widget has these primary behaviors:

  • The user can vertically fling the top view in the widget to display the next or previous view. This is a built-in StackView behavior.

  • Without any user interaction, the widget automatically advances through its views in sequence, like a slide show. This is due to the setting android:autoAdvanceViewId="@id/stack_view" in the res/xml/stackwidgetinfo.xml file. This setting applies to the view ID, which in this case is the view ID of the stack view.

  • If the user touches the top view, the widget displays the Toast message "Touched view n," where n is the index (position) of the touched view. For more discussion of how this is implemented, see Adding behavior to individual items.

Implement widgets with collections

To implement a widget with collections, follow the same procedure to implement any widget, followed by a few additional steps: modify the manifest, add a collection view to the widget layout, and modify your AppWidgetProvider subclass.

Manifest for widgets with collections

In addition to the requirements listed in Declaring a widget in the manifest, to make it possible for widgets with collections to bind to your RemoteViewsService, you must declare the service in your manifest file with the permission BIND_REMOTEVIEWS. This prevents other applications from freely accessing your widget's data. For example, when creating a widget that uses RemoteViewsService to populate a collection view, the manifest entry may look like this:

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

The line android:name="MyWidgetService" refers to your subclass of RemoteViewsService.

Layout for widgets with collections

The main requirement for your widget layout XML file is that it include one of the collection views: ListView, GridView, StackView, or AdapterViewFlipper. Here is the widget_layout.xml file for the StackWidget sample:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <StackView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/stack_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:loopViews="true" />
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        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>

Note that empty views must be siblings of the collection view for which the empty view represents empty state.

In addition to the layout file for your entire widget, you must create another layout file that defines the layout for each item in the collection (for example, a layout for each book in a collection of books). The StackWidget sample only has one layout file, widget_item.xml, since all items use the same layout.

AppWidgetProvider class for widgets with collections

As with a regular widget, the bulk of your code in your AppWidgetProvider subclass typically goes in onUpdate(). The major difference in your implementation for onUpdate() when creating a widget with collections is that you must call setRemoteAdapter(). This tells the collection view where to get its data. The RemoteViewsService can then return your implementation of RemoteViewsFactory, and the widget can serve up the appropriate data. When you call this method, you must pass an intent that points to your implementation of RemoteViewsService and the widget ID that specifies the widget to update.

For example, here's how the StackWidget sample implements the onUpdate() callback method to set the RemoteViewsService as the remote adapter for the widget collection:

Kotlin

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 will
        // provide 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 should 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)
}

Java

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 will
        // provide 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 should 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);
}

Persisting data

As described previously in this document, RemoteViewsService subclass provides the RemoteViewsFactory used to populate the remote collection view.

Specifically, you need to perform these steps:

  1. Subclass RemoteViewsService. RemoteViewsService is the service through which a remote adapter can request RemoteViews.

  2. In your RemoteViewsService subclass, include a class that implements the RemoteViewsFactory interface. RemoteViewsFactory is an interface for an adapter between a remote collection view (such as ListView, GridView, StackView) and the underlying data for that view. Your implementation is responsible for making aRemoteViewsobject for each item in the data set. This interface is a thin wrapper aroundAdapter`.

You can’t rely on a single instance of your service, or any data it contains, to persist. You should therefore not store any data in your RemoteViewsService (unless it is static). If you want your widget’s data to persist, the best approach is to use a ContentProvider whose data persists beyond the process lifecycle. For example, a grocery store widget could store the state of each grocery list item in a persistent location, such as a SQL database.

The primary contents of the RemoteViewsService implementation is its RemoteViewsFactory, described in the following section.

RemoteViewsFactory interface

Your custom class that implements the RemoteViewsFactory interface provides the widget with the data for the items in its collection. To do this, it combines your widget item XML layout file with a source of data. This source of data could be anything from a database to a simple array. In the StackWidget sample, the data source is an array of WidgetItems. The RemoteViewsFactory functions as an adapter to glue the data to the remote collection view.

The two most important methods you need to implement for your

RemoteViewsFactory subclass are onCreate() and getViewAt().

The system calls onCreate() when creating your factory for the first time. This is where you set up any connections and/or cursors to your data source. For example, the StackWidget sample uses onCreate() to initialize an array of WidgetItem objects. When your widget is active, the system accesses these objects using their index position in the array and the text they contain is displayed.

Here is an excerpt from the StackWidget sample's RemoteViewsFactory implementation that shows portions of the onCreate() method:

Kotlin

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() you setup any connections / cursors to your data
        // source. Heavy lifting, for example downloading or creating content
        // etc, should be deferred to onDataSetChanged() or getViewAt(). Taking
        // more than 20 seconds in this call will result in an ANR.
        widgetItems = List(REMOTE_VIEW_COUNT) { index -> WidgetItem("$index!") }
        ...
    }
    ...
}

Java

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() you setup any connections / cursors to your data
        // source. Heavy lifting, for example downloading or creating content
        // etc, should be deferred to onDataSetChanged() or getViewAt(). Taking
        // more than 20 seconds in this call will result in an ANR.
        for (int i = 0; i < REMOTE_VIEW_COUNT; i++) {
            widgetItems.add(new WidgetItem(i + "!"));
        }
        ...
    }
...

The RemoteViewsFactory method getViewAt() returns a RemoteViews object corresponding to the data at the specified position in the data set. Here is an excerpt from the StackWidget sample's RemoteViewsFactory implementation:

Kotlin

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)
    }
}

Java

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;
}

Add behavior to individual items

The preceding sections show you how to bind your data to your widget collection. But what if you want to add dynamic behavior to the individual items in your collection view?

As described in Handle events with the onUpdate() class, you normally use setOnClickPendingIntent() to set an object's click behavior—such as to cause a button to launch an Activity. But this approach is not allowed for child views in an individual collection item (to clarify, you could use setOnClickPendingIntent() to set up a global button in the Gmail widget that launches the app, for example, but not on the individual list items). Instead, to add click behavior to individual items in a collection, you use setOnClickFillInIntent(). This entails setting up a pending intent template for your collection view, and then setting a fill-in intent on each item in the collection via your RemoteViewsFactory.

This section uses the StackWidget sample to describe how to add behavior to individual items. In the StackWidget sample, if the user touches the top view, the widget displays the Toast message "Touched view n," where n is the index (position) of the touched view. This is how it works:

  • The StackWidgetProvider (an AppWidgetProvider subclass) creates a pending intent that has a custom action called TOAST_ACTION.

  • When the user touches a view, the intent is fired and it broadcasts TOAST_ACTION.

  • This broadcast is intercepted by the StackWidgetProvider's onReceive() method, and the widget displays the Toast message for the touched view. The data for the collection items is provided by the RemoteViewsFactory, via the RemoteViewsService.

Set up the pending intent template

The StackWidgetProvider (AppWidgetProvider subclass) sets up a pending intent. Individual items of a collection cannot set up their own pending intents. Instead, the collection as a whole sets up a pending intent template, and the individual items set a fill-in intent to create unique behavior on an item-by-item basis.

This class also receives the broadcast that is sent when the user touches a view. It processes this event in its onReceive() method. If the intent's action is TOAST_ACTION, the widget displays a Toast message for the current view.

Kotlin

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 to see 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 which
            // position of the item was clicked. 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 will
            // provide 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 we need to embed the extras
                // into the data so that the extras will not be 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 should 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 cannot 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 will have 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)
    }
}

Java

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 to see 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 which
            // position of the item was clicked. 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 will
            // provide 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 we need to
            // embed the extras into the data so that the extras will not be
            // 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
            // should 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 cannot 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 will have 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);
    }
}

Set the fill-in Intent

Your RemoteViewsFactory must set a fill-in intent on each item in the collection. This makes it possible to distinguish the individual on-click action of a given item. The fill-in intent is then combined with the PendingIntent template in order to determine the final intent that will be executed when the item is clicked.

Kotlin

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() you setup any connections / cursors to your data source. Heavy lifting,
        // for example downloading or creating content etc, should be deferred to onDataSetChanged()
        // or getViewAt(). Taking more than 20 seconds in this call will result 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)

            // Next, set a fill-intent, which will be used 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)
            ...
        }
    }
    ...
}

Java

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() you set up any connections / cursors to your data
        // source. Heavy lifting, for example downloading or creating
        // content etc, should be deferred to onDataSetChanged() or
        // getViewAt(). Taking more than 20 seconds in this call will result
        // 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 will always range 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);

        // Next, set a fill-intent, which will be used 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;
    }
    ...
}

Keep collection data fresh

The following figure illustrates the flow that occurs in a widget that uses collections when updates occur. It shows how the widget code interacts with the RemoteViewsFactory, and how you can trigger updates:

One feature of widgets that use collections is the ability to provide users with up-to-date content. For example, consider the Gmail widget, which provides users with a snapshot of their inbox. To make this possible, you need to be able to trigger your RemoteViewsFactory and collection view to fetch and display new data. You achieve this with the AppWidgetManager call notifyAppWidgetViewDataChanged(). This call results in a callback to your RemoteViewsFactory’s onDataSetChanged() method, which gives you the opportunity to fetch any new data. Note that you can perform processing-intensive operations synchronously within the onDataSetChanged() callback. You are guaranteed that this call will be completed before the metadata or view data is fetched from the RemoteViewsFactory. In addition, you can perform processing-intensive operations within the getViewAt() method. If this call takes a long time, the loading view (specified by the RemoteViewsFactory object's getLoadingView() method) will be displayed in the corresponding position of the collection view until it returns.

Use RemoteCollectionItems to pass along a collection directly

Android 12 (API level 31) adds the setRemoteAdapter(int viewId, RemoteViews.RemoteCollectionItems items) method, which lets your app pass along a collection directly when populating a collection view. If you set your adapter using this method, you do not need to implement a RemoteViewsFactory and you do not need to call notifyAppWidgetViewDataChanged(). In addition to making it easier to populate your adapter, this approach also removes the latency for populating new items when users scroll down the list to reveal a new item. This approach to setting the adapter is preferred as long as your set of collection items is relatively small in size. For example, this approach doesn't work well if your collection contains numerous Bitmaps being passed to setImageViewBitmap.

If the collection doesn’t use a constant set of layouts—that is, if some items are only sometimes present—use setViewTypeCount to specify the maximum number of unique layouts that the collection can contain. This allows the adapter to be reused across updates to your app widget.

Here’s an example of how to implement simplified RemoteViews collections.

Kotlin

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(R.layout.item_type_1))
            .addItem(/* id= */ ID_2, RemoteViews(R.layout.item_type_2))
            ...
            .setViewTypeCount(itemLayouts.count())
            .build()
)

Java

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(R.layout.item_type_1))
        .addItem(/* id= */ ID_2, new RemoteViews(R.layout.item_type_2))
        ...
        .setViewTypeCount(itemLayouts.size())
        .build()
);