Widgets (appwidgets) have been available on Android practically from the beginning (the AppWidgetProvider
class debuted in API level 3). They had their great moments before 2013, but slowly became irrelevant in the following years. When Apple added home screen widgets to iOS 14 (widgets had been around before but were somewhat cumbersome to access), the general reaction was beyond excitement. The good thing about the hype in the neighboring ecosystem is, that Google re-discovered its love for widgets. Android 12 contains a bunch of widget improvements. And we even got a new library that allows us to define the appwidget user interface using a declarative approach. While this series will briefly tackle both topics, too, its main focus is about something seemingly mundane, updating widgets. To understand why this justifies a whole article series, we need to start by looking at how appwdigets are built. The series is based on a small project called Battery Meter. You can find its source code on GitHub.
Architecture
Technically, appwidgets are BroadcastReceiver
subclasses. They receive a bunch of actions, for example AppWidgetManager.ACTION_APPWIDGET_UPDATE
and AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED
. To simplify the handling of widget-related actions, there's the AppWidgetProvider
class that extends BroadcastReceiver
. Its implementaton of onReceive()
calls methods like onUpdate()
and onAppWidgetOptionsChanged()
when corresponding actions are received. Therefore, in your apps you usually will want to extend AppWidgetProvider
.
<receiver
android:name=".XMLBatteryMeterWidgetReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_xmlbatterymeter_info" />
</receiver>
Appwidgets are registered in the manifest file. They declare an intent filter for android.appwidget.action.APPWIDGET_UPDATE
and provide meta data (with the name android.appwidget.provider
) that links to an xml file.
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="110dp"
android:minHeight="40dp"
android:targetCellWidth="2"
android:targetCellHeight="1"
android:maxResizeWidth="110dp"
android:maxResizeHeight="40dp"
android:updatePeriodMillis="1800000"
android:description="@string/xml_description"
android:previewLayout="@layout/widget_batterymeter_initial"
android:initialLayout="@layout/widget_batterymeter_initial"
android:resizeMode="none"
android:widgetCategory="home_screen">
</appwidget-provider>
The file contains the size of the widget, a description, a preview, layouts for different purposes, and a category. Starting with Android 4.2, widgets could be placed on the lock screen. Unfortunately this feature was removed with Android 5.
We'll focus on android:updatePeriodMillis
. It defines how often (in milliseconds) the widget wants to be updated. Update means redraw the contents reflecting the current status. A weather widget may want to get the latest forecast, the battery meter needs to update its gauge depending on the current battery level. The documentation states:
Now, you may be thinking
Wait a minute, does this really mean that widget update intervals are at least 30 minutes? 🤔
While this is certainly fine for a weather widget, a battery meter may significantly deviate from the actual battery level if the device was in heavy use. So, the answer basically can only be no.
Please recall the documentation said that updates requested with updatePeriodMillis will not be delivered more than once every 30 minutes. So, our widget configuration file can't ask for shorter intervals. There are other means, though. To understand them, we need to explore appwidget mechanics a little more.
All widget-related classes reside in the android.appwidget
package. AppWidgetHost
and AppWidgetHostView
are used by apps that want to embed widgets in their UI, like the home screen. You can find more information about this in Build a widget host. AppWidgetManager
updates widget state and allows us to get information about installed AppWidget providers and other widget-related state. We'll turn to this class in a minute. AppWidgetProvider
is, as I explained a little earlier, a convenience class to aid in implementing widgets. Finally, AppWidgetProviderInfo
describes the meta data for an installed AppWidgetProvider
. Usually you won't use this class but its alternative representation, the xml file that is referenced in the manifest file.
Let's recap:
-
AppWidgetProvider
andAppWidgetProviderInfo
(or its alternate representation define and implement the appwidget -
onUpdate()
of anAppWidgetProvider
instance is called when a widget should update itself -
AppWidgetManager
updates widget state and allows us to get information about installedAppWidgetProvider
s and other widget-related state
Using AppWidgetManager
To appreciate what AppWidgetManager
does, let's look at a minimal onUpdate()
implementation first.
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { appWidgetId ->
val views: RemoteViews = RemoteViews(
context.packageName,
R.layout.appwidget_provider_layout
).apply {
// update the views
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
onUpdate()
receives a list of widget ids. Each element represents one manifestation (copy) of the widget. Please recall that a given appwidget can be placed on screen several times. While iterating over the widget ids, we create a RemoteViews
instance. This object inflates a layout file and gives us access to its View
s. View
properties are set by special functions like setColor()
and setTextViewText()
. Once we have crafted our user interface, we update the widget by calling appWidgetManager.updateAppWidget()
.
Before we move on, please note that, whatever you do in onUpdate()
should be finished within 10 seconds. Widgets are BroadcastReceiver
instances and Android imposes certain limitations on onReceive()
. The documentation says:
This method is called when the BroadcastReceiver is receiving an Intent broadcast. During this time you can use the other methods on BroadcastReceiver to view/modify the current result values. This method is always called within the main thread of its process, unless you explicitly asked for it to be scheduled on a different thread using
Context.registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)
. When it runs on the main thread you should never perform long-running operations in it (there is a timeout of 10 seconds that the system allows before considering the receiver to be blocked and a candidate to be killed).
If you need more time, you can invoke goAsync()
in your onUpdate()
code (please recall that onUpdate()
is called from the onReceive()
implementation of AppWidgetProvider
). Please read the documentation to learn more.
Triggering updates
Here's something seemingly obvious: to update a widget more often than every 30 minutes, we just need to make sure that onUpdate()
is called. But how do we do that? If we wanted to directly invoke the method from the outside (for example from one of our apps' activities or services) we would need to pass an AppWidgetManager
instance and the list of widget ids. But more importantly, we would need to get an AppWidgetProvider
instance. Is this possible? Fortunately, it's not necessary, as AppWidgetManager
provides everything we need. Well, almost everything. Take a look.
We can obtain the AppWidgetManager
instance using AppWidgetManager.getInstance()
. And getAppWidgetIds()
returns, well, the list of appwidget ids. But all versions of updateAppWidget()
require a RemoteViews
instance. How do we get that? Please recall that we already do this, inside our onUpdate()
implementation. Allow me to remind you:
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { appWidgetId ->
val views: RemoteViews = RemoteViews(
context.packageName,
R.layout.appwidget_provider_layout
).apply {
// update the views
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
But everything that happens inside onUpdate()
is an implementation detail that should not be known to the outside world. Consequently, we should not even try to make the RemoteViews
instance accessible. Instead, I propose to move the complete code of onUpdate()
to a private top-level function with the same parameters and call this function from onUpdate()
.
Here's how XMLBatteryMeterWidgetReceiver
looks:
class XMLBatteryMeterWidgetReceiver : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
updateXMLBatteryMeterWidget(
context = context,
appWidgetManager = appWidgetManager,
appWidgetIds = appWidgetIds
)
}
}
And this is the new private top-level function:
private fun updateXMLBatteryMeterWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { appWidgetId ->
val views = RemoteViews(
context.packageName, R.layout.widget_batterymeter
).apply {
val percent = max(context.getBatteryStatusPercent(), 0.0F)
val resIds = listOf(
R.id.percent10,
...,
R.id.percent100
)
for (i in 0..9) {
setViewVisibility(
resIds[i], if (percent >= 10 + (i * 10))
View.VISIBLE
else
View.INVISIBLE
)
}
setTextViewText(R.id.percent, "${percent.toInt()} %")
val pendingIntent = PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT
or PendingIntent.FLAG_IMMUTABLE
)
setOnClickPendingIntent(R.id.root, pendingIntent)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
Please recall that View
properties cannot changed directly by invoking corresponding setters. Instead you need to use means provided by RemoteViews
, for example setViewVisibility()
and setTextViewText()
. Battery Meter divides the battery into ten segments and hides or shows segments based on the current battery level.
To update the widget from the outside, there is certainly still one bit missing.
fun Context.updateXMLBatteryMeterWidget() {
val component = ComponentName(this,
XMLBatteryMeterWidgetReceiver::class.java)
with(AppWidgetManager.getInstance(this)) {
val appWidgetIds = getAppWidgetIds(component)
updateXMLBatteryMeterWidget(
context = this@updateXMLBatteryMeterWidget,
appWidgetManager = this,
appWidgetIds = appWidgetIds
)
}
}
The outside world doesn't need to know anything about what's happening inside onUpdate()
. In an activity, I can use it like this:
override fun onPause() {
super.onPause()
updateXMLBatteryMeterWidget()
}
Looks really handy, right? Every time the activity is paused, the widget gets updated. Well, yes, but...
What's coming next
I somewhat vaguely said that we can update our widgets from activities and services. But what if our widget is not a companion, but basically all the app contains? A Weather widget doesn't necessarily need a main activity. Neither does a Battery Meter. Which app component would trigger widget updates in such scenarios?
Also, what happens to the widget if the user doesn't interact with it but only looks at it? Recent Android versions severely limit what apps can do if they have not been in the foreground for some time. Will the widget still be updated?
Let's find out...
Top comments (2)
Scheduled periodic work via WorkManager can request the same widget updates.
There is another gotcha, a widget app (without any foreground activity) would be considered a background and in background state, they can't have access to mobile data while data saver is on and all network calls would fail. Widgets using network data source would need some kind of activity to warn users to enable "Allow background data usage" and "Allow data usage while Data saver is on".
Thanks a lot for reading this article and sharing your thoughts. The topics you mentioned will be tackled in subsequent installments.