DEV Community

Cover image for Updating widgets - Introduction
Thomas Künneth
Thomas Künneth

Posted on • Updated on

Updating widgets - Introduction

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

Documentation of AppWidgetProviderInfo#updatePeriodMillis

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:

  1. AppWidgetProvider and AppWidgetProviderInfo (or its alternate representation define and implement the appwidget
  2. onUpdate() of an AppWidgetProvider instance is called when a widget should update itself
  3. AppWidgetManager updates widget state and allows us to get information about installed AppWidgetProviders 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)
  }
 }
Enter fullscreen mode Exit fullscreen mode

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 Views. 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.

Using AppWidgetManager

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)
  }
 }
Enter fullscreen mode Exit fullscreen mode

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
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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...


Source code

Top comments (2)

Collapse
 
nimbalabs profile image
Nimba Labs

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".

Collapse
 
tkuenneth profile image
Thomas Künneth

Thanks a lot for reading this article and sharing your thoughts. The topics you mentioned will be tackled in subsequent installments.