This page explains recommended practices for creating a more advanced widget for a better user experience.
Optimizations for updating widget content
Updating widget content can be computationally expensive. To save battery consumption, optimize the update type, frequency, and timing.
Types of widget updates
There are three ways to update a widget: a full update, a partial update, and, in the case of a collection widget, a data refresh. Each has different computational costs and ramifications.
The following describes each update type and provides code snippets for each.
Full update: call
AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews)
to fully update the widget. This replaces the previously providedRemoteViews
with a newRemoteViews
. This is the most computationally expensive update.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);
Partial update: call
AppWidgetManager.partiallyUpdateAppWidget
to update parts of the widget. This merges the newRemoteViews
with the previously providedRemoteViews
. This method is ignored if a widget doesn't receive at least one full update throughupdateAppWidget(int[], RemoteViews)
.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);
Collection data refresh: call
AppWidgetManager.notifyAppWidgetViewDataChanged
to invalidate the data of a collection view in your widget. This triggersRemoteViewsFactory.onDataSetChanged
. In the interim, the old data is displayed in the widget. You can safely perform expensive tasks synchronously with this method.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);
You can call these methods from anywhere in your app, as long as the app has the
same UID as the corresponding
AppWidgetProvider
class.
Determine how often to update a widget
Widgets are updated periodically depending on the value provided for the
updatePeriodMillis
attribute. The widget can update in response to user interaction, broadcast
updates, or both.
Update periodically
You can control the frequency of the periodic update by specifying a value for
AppWidgetProviderInfo.updatePeriodMillis
in the appwidget-provider
XML. Each
update triggers the AppWidgetProvider.onUpdate()
method, which is where you
can place the code to update the widget. However, consider the alternatives for
broadcast receiver updates described in a
following section if your widget needs to load data asynchronously or takes more
than 10 seconds to update, because after 10 seconds, the system considers a
BroadcastReceiver
to be non-responsive.
updatePeriodMillis
doesn't support values of less than 30 minutes. However, if
you want to disable periodic updates, you can specify 0.
You can let users adjust the frequency of updates in a configuration. For
example, they might want a stock ticker to update every 15 minutes or only four
times a day. In this case, set the updatePeriodMillis
to 0 and use
WorkManager
instead.
Update in response to a user interaction
Here are some recommended ways to update the widget based on user interaction:
From an activity of the app: directly call
AppWidgetManager.updateAppWidget
in response to a user interaction, such as a user's tap.From remote interactions, such as a notification or an app widget: construct a
PendingIntent
, then update the widget from the invokedActivity
,Broadcast
, orService
. You can choose your own priority. For example, if you select aBroadcast
for thePendingIntent
, you can choose a foreground broadcast to give theBroadcastReceiver
priority.
Update in response to a broadcast event
An example of a broadcast event that requires a widget to update is when the user takes a photo. In this case, you want to update the widget when a new photo is detected.
You can schedule a job with JobScheduler
and specify a broadcast as the
trigger using the
JobInfo.Builder.addTriggerContentUri
method.
You can also register a BroadcastReceiver
for the broadcast—for example,
listening for
ACTION_LOCALE_CHANGED
.
However, because this consumes device resources, use this with care and listen
only to the specific broadcast. With the introduction of broadcast
limitations in Android
7.0 (API level 24) and Android 8.0 (API level 26), apps can't register implicit
broadcasts in their manifests, with certain
exceptions.
Considerations when updating a widget from a BroadcastReceiver
If the widget is updated from a BroadcastReceiver
, including
AppWidgetProvider
, be aware of the following considerations regarding the
duration and priority of a widget update.
Duration of the update
As a rule, the system lets broadcast receivers, which usually run in the app’s main thread, run for up to 10 seconds before considering them non-responsive and triggering an Application Not Responding (ANR) error. If it takes longer to update the widget, consider the following alternatives:
Schedule a task using
WorkManager
.Give the receiver more time with the
goAsync
method. This lets receivers execute for 30 seconds.
See Security considerations and best practices for more information.
Priority of the update
By default, broadcasts—including those made using
AppWidgetProvider.onUpdate
—run as background processes. This means
overloaded system resources can cause a delay in the invocation of the broadcast
receiver. To prioritize the broadcast, make it a foreground process.
For example, add the
Intent.FLAG_RECEIVER_FOREGROUND
flag to the Intent
passed to the PendingIntent.getBroadcast
when the user
taps on a certain part of the widget.
Build accurate previews that include dynamic items
This section explains the recommended approach for displaying multiple items in
a widget preview for a widget with a collection
view—that is, a widget that uses a
ListView
, GridView
, or StackView
.
If your widget uses one of these views, creating a scalable preview by directly providing the actual widget layout degrades the experience when the widget preview displays no items. This occurs because collection view data is set dynamically at runtime, and it looks similar to the image shown in figure 1.
To make previews of widgets with collection views display properly in the widget
picker, we recommend maintaining a separate layout file designated for only the
preview. This separate layout file includes the actual widget layout and a
placeholder collection view with fake items. For example, you can mimic a
ListView
by providing a placeholder LinearLayout
with several fake list
items.
To illustrate an example for a ListView
, start with a separate layout file:
// 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>
Specify the preview layout file when providing the previewLayout
attribute of
the AppWidgetProviderInfo
metadata. You still specify the actual widget layout
for the initialLayout
attribute and use the actual widget layout when
constructing a RemoteViews
at runtime.
<appwidget-provider
previewLayout="@layout/widget_previe"
initialLayout="@layout/widget_view" />
Complex list items
The example in the previous section provides fake list items, because the list
items are TextView
objects. It can be
more complex to provide fake items if the items are complex layouts.
Consider a list item that is defined in widget_list_item.xml
and consists of
two TextView
objects:
<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>
To provide fake list items, you can include the layout multiple times, but this causes each list item to be identical. To provide unique list items, follow these steps:
Create a set of attributes for the text values:
<resources> <attr name="widgetTitle" format="string" /> <attr name="widgetContent" format="string" /> </resources>
Use these attributes to set the text:
<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>
Create as many styles as required for the preview. Redefine the values in each style:
<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>
Apply the styles on the fake items in the preview layout:
<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>