高度なウィジェットを作成する

このページでは、ユーザー エクスペリエンスを向上させるため、より高度なウィジェットを作成するためのおすすめの方法について説明します。

ウィジェット コンテンツの更新の最適化

ウィジェット コンテンツの更新は、計算コストが高額になることがあります。バッテリー消費を抑えるには、更新タイプ、頻度、タイミングを最適化します。

ウィジェットの更新の種類

ウィジェットを更新するには、完全な更新、部分的な更新、コレクション ウィジェットの場合はデータの更新の 3 つの方法があります。それぞれ計算コストと影響が異なります。

以下では、各更新タイプの説明と、それぞれのコード スニペットを示します。

  • 完全な更新: AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews) を呼び出して、ウィジェットを完全に更新します。これにより、以前に提供された RemoteViews が新しい RemoteViews に置き換えられます。これは最も計算コストの高い更新です。

    Kotlin

    val appWidgetManager = AppWidgetManager.getInstance(context)
    val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also {
    setTextViewText(R.id.textview_widget_layout1, "Updated text1")
    setTextViewText(R.id.textview_widget_layout2, "Updated text2")
    }
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews)

    Java

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widgetlayout);
    remoteViews.setTextViewText(R.id.textview_widget_layout1, "Updated text1");
    remoteViews.setTextViewText(R.id.textview_widget_layout2, "Updated text2");
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
  • 部分更新: AppWidgetManager.partiallyUpdateAppWidget を呼び出して、ウィジェットの一部を更新します。これにより、新しい RemoteViews が以前に指定された RemoteViews と統合されます。ウィジェットが updateAppWidget(int[], RemoteViews) を介して完全な更新を 1 回以上受信しなかった場合、このメソッドは無視されます。

    Kotlin

    val appWidgetManager = AppWidgetManager.getInstance(context)
    val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also {
    setTextViewText(R.id.textview_widget_layout, "Updated text")
    }
    appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews)

    Java

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widgetlayout);
    remoteViews.setTextViewText(R.id.textview_widget_layout, "Updated text");
    appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews);
  • コレクション データの更新: AppWidgetManager.notifyAppWidgetViewDataChanged を呼び出して、ウィジェットのコレクション ビューのデータを無効にします。これにより、RemoteViewsFactory.onDataSetChanged がトリガーされます。その間、ウィジェットには古いデータが表示されます。この方法では、費用のかかるタスクを同期的に安全に実行できます。

    Kotlin

    val appWidgetManager = AppWidgetManager.getInstance(context)
    appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)

    Java

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview);

アプリが対応する AppWidgetProvider クラスと同じ UID を持っている限り、これらのメソッドはアプリ内の任意の場所で呼び出すことができます。

ウィジェットの更新頻度を決定する

ウィジェットは、updatePeriodMillis 属性に指定された値に応じて定期的に更新されます。ウィジェットは、ユーザー操作、ブロードキャスト アップデート、またはその両方に応じて更新できます。

定期的に更新する

定期的な更新の頻度は、appwidget-provider XML で AppWidgetProviderInfo.updatePeriodMillis の値を指定することで制御できます。各更新で AppWidgetProvider.onUpdate() メソッドがトリガーされます。このメソッドに、ウィジェットを更新するコードを配置できます。ただし、ウィジェットでデータを非同期に読み込む必要がある場合や、更新に 10 秒以上かかる場合は、次のセクションで説明するブロードキャスト レシーバの更新の代替方法を検討してください。10 秒経過すると、システムは BroadcastReceiver を応答なしと見なします。

updatePeriodMillis は 30 分未満の値をサポートしていません。ただし、定期的な更新を無効にするには、0 を指定します。

ユーザーが設定で更新頻度を調整できるようにします。たとえば、株価情報を 15 分おきに更新する、1 日 4 回のみ更新する、といった設定ができます。この場合は、updatePeriodMillis を 0 に設定し、代わりに WorkManager を使用します。

ユーザーの操作に応じて更新する

ユーザー操作に基づいてウィジェットを更新するおすすめの方法をいくつかご紹介します。

  • アプリのアクティビティから: ユーザー操作(ユーザーのタップなど)に応じて AppWidgetManager.updateAppWidget を直接呼び出します。

  • 通知やアプリ ウィジェットなどのリモート操作から: PendingIntent を作成してから、呼び出された ActivityBroadcast、または Service からウィジェットを更新します。優先度を自分で選択できます。たとえば、PendingIntentBroadcast を選択した場合は、フォアグラウンド ブロードキャストを選択して BroadcastReceiver に優先度を設定できます。

ブロードキャスト イベントに応答して更新する

ウィジェットの更新が必要なブロードキャスト イベントの例としては、ユーザーが写真を撮影したときなどがあります。この場合、新しい写真が検出されたときにウィジェットを更新します。

JobScheduler でジョブのスケジュールを設定し、JobInfo.Builder.addTriggerContentUri メソッドを使用してブロードキャストをトリガーとして指定できます。

ブロードキャスト用に BroadcastReceiver を登録することもできます(ACTION_LOCALE_CHANGED をリッスンするなど)。ただし、これはデバイスのリソースを消費するため、慎重に使用し、特定のブロードキャストのみを聞くようにしてください。Android 7.0(API レベル 24)と Android 8.0(API レベル 26)でブロードキャストの制限が導入されたため、アプリはマニフェストで暗黙的ブロードキャストを登録できなくなりました(一部の例外を除く)。

BroadcastReceiver からウィジェットを更新する際の考慮事項

ウィジェットが BroadcastReceiverAppWidgetProvider など)から更新される場合は、ウィジェットの更新時間と優先度に関する次の考慮事項に注意してください。

更新の所要時間

通常、アプリのメインのスレッドで実行されるブロードキャスト レシーバは、最大 10 秒間実行された後、応答なしと見なされ、アプリケーション応答なし(ANR)エラーがトリガーされます。ブロードキャストを処理中にメインスレッドをブロックしないようにするには、goAsync メソッドを使用します。ウィジェットの更新に時間がかかる場合は、WorkManager を使用してタスクのスケジュール設定を検討してください。

Caution: Any work you do here blocks further broadcasts until it completes,
so it can slow the receiving of later events.

詳細については、セキュリティに関する考慮事項とベスト プラクティスをご覧ください。

アップデートの優先度

デフォルトでは、ブロードキャスト(AppWidgetProvider.onUpdate を使用して作成されたものを含む)はバックグラウンド プロセスとして実行されます。つまり、システム リソースが過負荷になると、ブロードキャスト レシーバーの呼び出しが遅れる可能性があります。ブロードキャストを優先するには、フォアグラウンド プロセスにします。

たとえば、ユーザーがウィジェットの特定の部分をタップしたときに PendingIntent.getBroadcast に渡される IntentIntent.FLAG_RECEIVER_FOREGROUND フラグを追加します。

動的アイテムを含む正確なプレビューを作成する

図 1: リストアイテムが表示されていないウィジェットのプレビュー。

このセクションでは、コレクション ビューを使用するウィジェット(ListViewGridViewStackView を使用するウィジェット)のウィジェット プレビューに複数のアイテムを表示するための推奨アプローチについて説明します。

ウィジェットでこれらのビューのいずれかを使用している場合、実際のウィジェット レイアウトを直接指定してスケーラブルなプレビューを作成すると、ウィジェット プレビューにアイテムが表示されない場合、エクスペリエンスが低下します。これは、コレクション ビューのデータが実行時に動的に設定されるためで、図 1 に示す画像のようになります。

コレクション ビューを含むウィジェットのプレビューをウィジェット選択ツールで正しく表示するには、プレビュー専用の個別のレイアウト ファイルを維持することをおすすめします。この個別のレイアウト ファイルには、実際のウィジェット レイアウトと、偽のアイテムを含むプレースホルダ コレクション ビューが含まれています。たとえば、複数の偽のリストアイテムを含むプレースホルダ LinearLayout を指定して、ListView を模倣できます。

ListView の例を説明するために、まず個別のレイアウト ファイルを作成します。

// res/layout/widget_preview.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:background="@drawable/widget_background"
   android:orientation="vertical">

    // Include the actual widget layout that contains ListView.
    <include
        layout="@layout/widget_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    // The number of fake items you include depends on the values you provide
    // for minHeight or targetCellHeight in the AppWidgetProviderInfo
    // definition.

    <TextView android:text="@string/fake_item1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="?attr/appWidgetInternalPadding" />

    <TextView android:text="@string/fake_item2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="?attr/appWidgetInternalPadding" />

</LinearLayout>

AppWidgetProviderInfo メタデータの previewLayout 属性を指定するときに、プレビュー レイアウト ファイルを指定します。initialLayout 属性には引き続き実際のウィジェット レイアウトを指定し、実行時に RemoteViews を構築するときに実際のウィジェット レイアウトを使用します。

<appwidget-provider
    previewLayout="@layout/widget_previe"
    initialLayout="@layout/widget_view" />

複雑なリストアイテム

リストアイテムは TextView オブジェクトであるため、前のセクションの例では偽のリストアイテムが提供されています。アイテムが複雑なレイアウトの場合は、偽のアイテムを提供する作業が複雑になる可能性があります。

widget_list_item.xml で定義され、2 つの TextView オブジェクトで構成されるリストアイテムについて考えてみましょう。

<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <TextView android:id="@id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/fake_title" />

    <TextView android:id="@id/content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/fake_content" />
</LinearLayout>

偽のリストアイテムを指定するには、レイアウトを複数回含めることができますが、その場合、各リストアイテムは同じになります。一意のリストアイテムを指定するには、次の操作を行います。

  1. テキスト値の属性セットを作成します。

    <resources>
        <attr name="widgetTitle" format="string" />
        <attr name="widgetContent" format="string" />
    </resources>
    
  2. 次の属性を使用してテキストを設定します。

    <LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
        <TextView android:id="@id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="?widgetTitle" />
    
        <TextView android:id="@id/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="?widgetContent" />
    </LinearLayout>
    
  3. プレビューに必要な数だけスタイルを作成します。各スタイルの値を再定義します。

    <resources>
    
        <style name="Theme.Widget.ListItem">
            <item name="widgetTitle"></item>
            <item name="widgetContent"></item>
        </style>
        <style name="Theme.Widget.ListItem.Preview1">
            <item name="widgetTitle">Fake Title 1</item>
            <item name="widgetContent">Fake content 1</item>
        </style>
        <style name="Theme.Widget.ListItem.Preview2">
            <item name="widgetTitle">Fake title 2</item>
            <item name="widgetContent">Fake content 2</item>
        </style>
    
    </resources>
    
  4. プレビュー レイアウトの偽のアイテムにスタイルを適用します。

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="wrap_content" ...>
    
        <include layout="@layout/widget_view" ... />
    
        <include layout="@layout/widget_list_item"
            android:theme="@style/Theme.Widget.ListItem.Preview1" />
    
        <include layout="@layout/widget_list_item"
            android:theme="@style/Theme.Widget.ListItem.Preview2" />
    
    </LinearLayout>