コレクション ウィジェットを使用する

コレクション ウィジェットは、ギャラリー アプリの画像のコレクション、ニュースアプリの記事、通信アプリのメッセージなど、同じタイプの多くの要素の表示に特化しています。コレクション ウィジェットは通常、2 つのユースケース(コレクションを閲覧し、コレクションの要素をその詳細ビューで開く)に重点を置きます。コレクション ウィジェットは上下にスクロールできます。

これらのウィジェットは、RemoteViewsService を使用して、コンテンツ プロバイダなどのリモートデータに基づくコレクションを表示します。ウィジェットは、次のいずれかのビュータイプ(コレクション ビュー)を使用してデータを表示します。

ListView
垂直方向にスクロールするリストにアイテムを表示するビュー。
GridView
2 次元のスクロール グリッドにアイテムを表示するビュー
StackView
積み重ねたカードビュー(Rolodex のようなもので、前のカードを上または下にフリックすると、前のカードまたは次のカードが表示されます。
AdapterViewFlipper
2 つ以上のビューの間でアニメーション化する、アダプタに基づくシンプルな ViewAnimator。お子様は一度に 1 人だけ表示されます。

これらのコレクション ビューは、リモートデータに基づくコレクションを表示するため、Adapter を使用してユーザー インターフェースをデータにバインドします。Adapter は、データセットの個々のアイテムを個々の View オブジェクトにバインドします。

また、これらのコレクション ビューはアダプターを基盤としているため、Android フレームワークにはウィジェットでの使用をサポートする追加のアーキテクチャを含める必要があります。ウィジェットのコンテキストでは、AdapterRemoteViewsFactory に置き換えられます。これは、Adapter インターフェースのシンラッパーです。コレクション内の特定のアイテムに対してリクエストされると、RemoteViewsFactory はコレクションのアイテムを RemoteViews オブジェクトとして作成し、返します。ウィジェットにコレクション ビューを含めるには、RemoteViewsServiceRemoteViewsFactory を実装します。

RemoteViewsService は、リモート アダプタが RemoteViews オブジェクトをリクエストできるようにするサービスです。RemoteViewsFactory は、コレクション ビュー(ListViewGridViewStackView など)とそのビューの基になるデータの間のアダプタ用のインターフェースです。StackWidget サンプルから、このサービスとインターフェースを実装するボイラープレート コードの例を以下に示します。

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.

}

サンプルアプリ

このセクションのコードの抜粋は、StackWidget サンプルからも引用されています。

図 1. StackWidget

このサンプルは、0 ~ 9 の値を表示する 10 個のビューのスタックで構成されています。サンプル ウィジェットの主な動作は次のとおりです。

  • ユーザーは、ウィジェットのトップビューを垂直方向にフリングすることで、次または前のビューを表示できます。これは組み込みの StackView の動作です。

  • ユーザーによる操作なしで、ウィジェットはスライドショーのように自動的にビューを順番に進めます。これは、res/xml/stackwidgetinfo.xml ファイルの android:autoAdvanceViewId="@id/stack_view" の設定によるものです。この設定はビュー ID(この場合はスタックビューのビュー ID)に適用されます。

  • ユーザーが上部のビューにタッチすると、ウィジェットは Toast のメッセージ「タッチビュー n」を表示します。ここで、n はタッチビューのインデックス(位置)です。動作の実装方法について詳しくは、個々のアイテムに動作を追加するのセクションをご覧ください。

コレクションを使用してウィジェットを実装する

コレクションを使用するウィジェットを実装するには、ウィジェットを実装する手順に沿って、マニフェストの変更、ウィジェット レイアウトへのコレクション ビューの追加、AppWidgetProvider サブクラスの変更を行います。

コレクションを含むウィジェットのマニフェスト

マニフェストでウィジェットを宣言するに記載されている要件以外にも、コレクションを持つウィジェットを RemoteViewsService にバインドできるようにする必要があります。これを行うには、マニフェスト ファイルで、権限 BIND_REMOTEVIEWS を使用してサービスを宣言します。これにより、他のアプリはウィジェットのデータに自由にアクセスできなくなります。

たとえば、RemoteViewsService を使用してコレクション ビューにデータを入力するウィジェットを作成する場合、マニフェスト エントリは次のようになります。

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

この例では、android:name="MyWidgetService"RemoteViewsService のサブクラスを指します。

コレクションを使用したウィジェットのレイアウト

ウィジェット レイアウト XML ファイルの主な要件は、コレクション ビュー ListViewGridViewStackViewAdapterViewFlipper のいずれかが含まれていることです。StackWidget サンプルwidget_layout.xml ファイルは次のとおりです。

<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 は 1 つだけです。

コレクションを持つウィジェットの AppWidgetProvider クラス

通常のウィジェットと同様に、通常、AppWidgetProvider サブクラス内のコードの大部分は onUpdate() に配置されます。コレクションを使用してウィジェットを作成する場合の onUpdate() の実装における主な違いは、setRemoteAdapter() を呼び出す必要があることです。これにより、データを取得する場所をコレクション ビューに伝えます。RemoteViewsServiceRemoteViewsFactory の実装を返し、ウィジェットは適切なデータを提供できます。このメソッドを呼び出すときは、RemoteViewsService の実装をポイントするインテントと、更新するウィジェットを指定するウィジェット ID を渡します。

たとえば、次のように StackWidget サンプルで onUpdate() コールバック メソッドを実装し、RemoteViewsService をウィジェット コレクションのリモート アダプターとして設定しています。

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

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
        // 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. サブクラス RemoteViewsServiceRemoteViewsService は、リモート アダプタが RemoteViews をリクエストする際に使用するサービスです。

  2. RemoteViewsService サブクラスに、RemoteViewsFactory インターフェースを実装するクラスを含めます。RemoteViewsFactory は、リモート コレクション ビュー(ListViewGridViewStackView など)とそのビューの基になるデータの間のアダプターのインターフェースです。実装では、データセット内の各アイテム用に RemoteViews オブジェクトを作成します。このインターフェースは Adapter のシンラッパーです。

サービスの単一のインスタンスや、サービスに含まれるデータに依存して存続することはできません。静的でない限り、RemoteViewsService にはデータを保存しないでください。ウィジェットのデータを保持したい場合は、プロセスのライフサイクルを超えてデータを保持する ContentProvider を使用することをおすすめします。たとえば、食料品店ウィジェットでは、各食料品リストアイテムの状態を SQL データベースなどの永続的な場所に保存できます。

RemoteViewsService 実装の主な内容は RemoteViewsFactory です。これについては次のセクションで説明します。

RemoteViewsFactory インターフェース

RemoteViewsFactory インターフェースを実装するカスタムクラスは、コレクション内のアイテムのデータをウィジェットに提供します。これを行うために、ウィジェット アイテムの XML レイアウト ファイルをデータソースと組み合わせます。このデータソースは、データベースから単純な配列まで、どのようなものでもかまいません。StackWidget サンプルでは、データソースは WidgetItems の配列です。RemoteViewsFactory は、データをリモート コレクション ビューに接着するアダプターとして機能します。

RemoteViewsFactory サブクラスに実装する必要がある最も重要なメソッドは、onCreate()getViewAt() の 2 つです。

初めてファクトリーを作成するときに、システムによって onCreate() が呼び出されます。ここでデータソースへの接続やカーソルを設定します。たとえば、StackWidget サンプルでは、onCreate() を使用して WidgetItem オブジェクトの配列を初期化します。ウィジェットがアクティブな場合、システムは配列内のインデックス位置を使用してこれらのオブジェクトにアクセスし、オブジェクトに含まれているテキストを表示します。

StackWidget サンプルの RemoteViewsFactory 実装からの抜粋を、onCreate() メソッドの一部を示します。

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(), 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!") }
        ...
    }
    ...
}

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(), 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() は、データセット内の指定された position にあるデータに対応する RemoteViews オブジェクトを返します。StackWidget サンプルの RemoteViewsFactory 実装からの抜粋を以下に示します。

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

個々のアイテムに動作を追加する

前のセクションでは、データをウィジェット コレクションにバインドする方法について説明しました。しかし、コレクション ビュー内の個々のアイテムに動的な動作を追加したい場合はどうすればよいでしょうか。

onUpdate() クラスでイベントを処理するで説明しているように、通常は setOnClickPendingIntent() を使用して、オブジェクトのクリック動作(ボタンによる Activity の起動など)を設定します。ただし、個別のコレクション アイテムの子ビューに対してこのアプローチを使用することはできません。たとえば、setOnClickPendingIntent() を使用すると、Gmail ウィジェットにアプリなどを起動するグローバル ボタンを設定できますが、個々のリストアイテムには設定できません。

コレクション内の個々のアイテムにクリック動作を追加するには、setOnClickFillInIntent() を使用します。これには、コレクション ビュー用にペンディング インテント テンプレートをセットアップしてから、RemoteViewsFactory を介してコレクション内の各アイテムにフィルイン インテントを設定する必要があります。

このセクションでは、StackWidget サンプルを使用して、個々のアイテムに動作を追加する方法について説明します。StackWidget サンプルでは、ユーザーが上面のビューにタッチすると、ウィジェットに「タッチしたビューの n」というメッセージ Toast が表示されます。ここで、n はタッチされたビューのインデックス(位置)です。仕組みは次のとおりです。

  • StackWidgetProviderAppWidgetProvider サブクラス)は、TOAST_ACTION というカスタム アクションを持つペンディング インテントを作成します。

  • ユーザーがビューをタップすると、インテントが起動し、TOAST_ACTION がブロードキャストされます。

  • このブロードキャストは、StackWidgetProvider クラスの onReceive() メソッドによってインターセプトされ、ウィジェットはタッチビューに関する Toast メッセージを表示します。コレクション アイテムのデータは、RemoteViewsService を通じて RemoteViewsFactory によって提供されます。

ペンディング インテント テンプレートをセットアップする

StackWidgetProviderAppWidgetProvider サブクラス)はペンディング インテントを設定します。コレクションの個々のアイテムが独自のペンディング インテントを設定することはできません。代わりに、コレクション全体でペンディング インテント テンプレートを設定し、個々のアイテムがフィルイン インテントを設定して、アイテムごとに固有の動作を作成します。

このクラスは、ユーザーがビューをタップしたときに送信されるブロードキャストも受け取ります。このイベントを onReceive() メソッドで処理します。インテントのアクションが TOAST_ACTION の場合、ウィジェットは現在のビューに関する Toast メッセージを表示します。

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

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 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 テンプレートと組み合わされて、アイテムがタップされたときに実行される最終的なインテントが決定されます。

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(), 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)
            ...
        }
    }
    ...
}

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(), 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() を呼び出します。この呼び出しにより、RemoteViewsFactory オブジェクトの onDataSetChanged() メソッドにコールバックされ、新しいデータをフェッチできます。

処理負荷の高いオペレーションは、onDataSetChanged() コールバック内で同期的に実行できます。この呼び出しは、メタデータまたはビューデータが RemoteViewsFactory から取得される前に完了することが保証されます。また、getViewAt() メソッド内で処理負荷の高いオペレーションを実行することもできます。この呼び出しに時間がかかる場合は、RemoteViewsFactory オブジェクトの getLoadingView() メソッドで指定された読み込みビューが、返されるまでコレクション ビューの対応する位置に表示されます。

RemoteCollectionItems を使用してコレクションを直接渡す

Android 12(API レベル 31)では、setRemoteAdapter(int viewId, RemoteViews.RemoteCollectionItems items) メソッドが追加されています。これにより、アプリはコレクション ビューにデータを入力する際に、コレクションを直接渡すことができます。このメソッドを使ってアダプタを設定する場合、RemoteViewsFactory を実装する必要はなく、notifyAppWidgetViewDataChanged() を呼び出す必要もありません。

この方法では、アダプタの入力が簡単になるだけでなく、ユーザーがリストを下にスクロールして新しいアイテムを表示したときに、新しいアイテムを入力するレイテンシもなくなります。一連のコレクション アイテムが比較的小さい場合に限り、このアプローチでアダプターを設定することをおすすめします。ただし、たとえば、コレクションに setImageViewBitmap に渡される Bitmaps が多数含まれている場合、この方法はうまく機能しません。

コレクションで一定数のレイアウト セットを使用しない場合(一部のアイテムがたまにしか存在しない場合)は、setViewTypeCount を使用して、コレクションに含めることができる一意のレイアウトの最大数を指定します。これにより、アプリ ウィジェットの更新全体でアダプタを再利用できます。

簡素化された RemoteViews コレクションを実装する方法の例を次に示します。

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