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

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

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

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

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

ウィジェットを更新する方法は 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 を作成してから、呼び出された ActivityBroadcastService からウィジェットを更新します。優先度は自由に設定できます。たとえば、PendingIntentBroadcast を選択した場合、フォアグラウンド ブロードキャストを選択して、BroadcastReceiver を優先できます。

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

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

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

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

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

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

更新の期間

原則として、通常はアプリのメインスレッドで実行されるブロードキャスト レシーバを最大 10 秒間実行できると、そのブロードキャスト レシーバは応答していないと見なされてアプリケーション応答なし(ANR)エラーがトリガーされます。ウィジェットの更新に時間がかかる場合は、次の方法を検討してください。

  • WorkManager を使用してタスクのスケジュールを設定します。

  • goAsync メソッドを使用して、レシーバーの時間を長くします。これにより、レシーバーが 30 秒間実行されます。

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

更新の優先度

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

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

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

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

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

ウィジェットでこれらのビューのいずれかを使用している場合、実際のウィジェット レイアウトを直接提供することでスケーラブルなプレビューを作成すると、ウィジェット プレビューにアイテムが表示されなくてもエクスペリエンスが低下します。これは、コレクション ビュー データが実行時に動的に設定され、図 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>