
Widgets (appwidgets) have been available on Android practically from the beginning (theAppWidgetProvider
class debuted in API level 3). They had theirgreat 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 containsa bunch of widget improvements. And we even gota 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 calledBattery Meter. You can find its source code onGitHub.
Architecture
Technically, appwidgets areBroadcastReceiver
subclasses. They receive a bunch of actions, for exampleAppWidgetManager.ACTION_APPWIDGET_UPDATE
andAppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED
. To simplify the handling of widget-related actions, there's theAppWidgetProvider
class that extendsBroadcastReceiver
. Its implementaton ofonReceive()
calls methods likeonUpdate()
andonAppWidgetOptionsChanged()
when corresponding actions are received. Therefore, in your apps you usually will want to extendAppWidgetProvider
.
<receiverandroid:name=".XMLBatteryMeterWidgetReceiver"android:exported="false"><intent-filter><actionandroid:name="android.appwidget.action.APPWIDGET_UPDATE"/></intent-filter><meta-dataandroid:name="android.appwidget.provider"android:resource="@xml/widget_xmlbatterymeter_info"/></receiver>
Appwidgets are registered in the manifest file. They declare an intent filter forandroid.appwidget.action.APPWIDGET_UPDATE
and provide meta data (with the nameandroid.appwidget.provider
) that links to an xml file.
<appwidget-providerxmlns: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 onandroid:updatePeriodMillis
. It defines how often (in milliseconds) the widget wants to be updated.Update meansredraw 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. Thedocumentation 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 beno.
Please recall the documentation said that updatesrequested 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 theandroid.appwidget
package.AppWidgetHost
andAppWidgetHostView
are used by apps that want to embed widgets in their UI, like the home screen. You can find more information about this inBuild 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 installedAppWidgetProvider
. 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 appwidgetonUpdate()
of anAppWidgetProvider
instance is called when a widget should update itselfAppWidgetManager
updates widget state and allows us to get information about installedAppWidgetProvider
s and other widget-related state
UsingAppWidgetManager
To appreciate whatAppWidgetManager
does, let's look at a minimalonUpdate()
implementation first.
overridefunonUpdate(context:Context,appWidgetManager:AppWidgetManager,appWidgetIds:IntArray){appWidgetIds.forEach{appWidgetId->valviews: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 aRemoteViews
instance. This object inflates a layout file and gives us access to itsView
s.View
properties are set by special functions likesetColor()
andsetTextViewText()
. Once we have crafted our user interface, we update the widget by callingappWidgetManager.updateAppWidget()
.
Before we move on, please note that, whatever you do inonUpdate()
should be finished within 10 seconds. Widgets areBroadcastReceiver
instances and Android imposes certain limitations ononReceive()
. The documentationsays:
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 invokegoAsync()
in youronUpdate()
code (please recall thatonUpdate()
is called from theonReceive()
implementation ofAppWidgetProvider
). Please read thedocumentation 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 thatonUpdate()
is called. But how do we do that? If we wanted to directly invoke the methodfrom the outside (for example from one of our apps' activities or services) we would need to pass anAppWidgetManager
instance and the list of widget ids. But more importantly, we would need to get anAppWidgetProvider
instance. Is this possible? Fortunately, it's not necessary, asAppWidgetManager
provides everything we need. Well, almost everything. Take a look.
We can obtain theAppWidgetManager
instance usingAppWidgetManager.getInstance()
. AndgetAppWidgetIds()
returns, well, the list of appwidget ids. But all versions ofupdateAppWidget()
require aRemoteViews
instance. How do we get that? Please recall that we already do this, inside ouronUpdate()
implementation. Allow me to remind you:
overridefunonUpdate(context:Context,appWidgetManager:AppWidgetManager,appWidgetIds:IntArray){appWidgetIds.forEach{appWidgetId->valviews:RemoteViews=RemoteViews(context.packageName,R.layout.appwidget_provider_layout).apply{// update the views}appWidgetManager.updateAppWidget(appWidgetId,views)}}
But everything that happens insideonUpdate()
is an implementation detail that should not be known to the outside world. Consequently, we should not even try to make theRemoteViews
instance accessible. Instead, I propose to move the complete code ofonUpdate()
to a private top-level function with the same parameters and call this function fromonUpdate()
.
Here's howXMLBatteryMeterWidgetReceiver
looks:
classXMLBatteryMeterWidgetReceiver:AppWidgetProvider(){overridefunonUpdate(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:
privatefunupdateXMLBatteryMeterWidget(context:Context,appWidgetManager:AppWidgetManager,appWidgetIds:IntArray){appWidgetIds.forEach{appWidgetId->valviews=RemoteViews(context.packageName,R.layout.widget_batterymeter).apply{valpercent=max(context.getBatteryStatusPercent(),0.0F)valresIds=listOf(R.id.percent10,...,R.id.percent100)for(iin0..9){setViewVisibility(resIds[i],if(percent>=10+(i*10))View.VISIBLEelseView.INVISIBLE)}setTextViewText(R.id.percent,"${percent.toInt()} %")valpendingIntent=PendingIntent.getActivity(context,0,Intent(context,MainActivity::class.java),PendingIntent.FLAG_UPDATE_CURRENTorPendingIntent.FLAG_IMMUTABLE)setOnClickPendingIntent(R.id.root,pendingIntent)}appWidgetManager.updateAppWidget(appWidgetId,views)}}
Please recall thatView
properties cannot changed directly by invoking corresponding setters. Instead you need to use means provided byRemoteViews
, for examplesetViewVisibility()
andsetTextViewText()
.Battery Meter divides the battery into ten segments and hides or shows segments based on the current battery level.
To update the widgetfrom the outside, there is certainly still one bit missing.
funContext.updateXMLBatteryMeterWidget(){valcomponent=ComponentName(this,XMLBatteryMeterWidgetReceiver::class.java)with(AppWidgetManager.getInstance(this)){valappWidgetIds=getAppWidgetIds(component)updateXMLBatteryMeterWidget(context=this@updateXMLBatteryMeterWidget,appWidgetManager=this,appWidgetIds=appWidgetIds)}}
The outside world doesn't need to know anything about what's happening insideonUpdate()
. In an activity, I can use it like this:
overridefunonPause(){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? AWeather widget doesn't necessarily need a main activity. Neither does aBattery 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 onlylooks 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".

- LocationGermany
- WorkSenior Android Developer @snappmobile.io
- Joined
Thanks a lot for reading this article and sharing your thoughts. The topics you mentioned will be tackled in subsequent installments.
For further actions, you may consider blocking this person and/orreporting abuse