Updating widgets - Introduction
Tags: android, widgets, appwidgets
Published: 2023-03-27 11:20:26

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](https://trends.google.com/trends/explore?date=2009-01-01%202023-03-01&q=appwidget), 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](https://developer.android.com/about/versions/12/features/widgets). And we even got [a new library](https://developer.android.com/jetpack/androidx/releases/glance) 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](https://github.com/tkuenneth/battery_meter).

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

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

```xml
<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](https://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo#updatePeriodMillis) states:

![Documentation of AppWidgetProviderInfo#updatePeriodMillis](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fqzle0m0wbcnc78o1fqu.png)

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](https://developer.android.com/develop/ui/views/appwidgets/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 `AppWidgetProvider`s and other widget-related state

### Using `AppWidgetManager`

To appreciate what `AppWidgetManager` does, let's look at a minimal `onUpdate()` implementation first. 

```kotlin
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](https://developer.android.com/reference/android/content/BroadcastReceiver#onReceive(android.content.Context,%20android.content.Intent)):

> 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](https://developer.android.com/reference/android/content/BroadcastReceiver#goAsync()) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rymh6u62jgxuzezo7q1q.png)

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:

```kotlin
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:

```kotlin
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:

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

```kotlin
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:

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

---

[Source code](https://github.com/tkuenneth/battery_meter)