Home screen widgets are part of many Android apps, they allow users to view data on the home screen without having to launch the app and actively interact with it. For this reason, they are especially popular among weather forecast apps, news feeds, stocks & cryptocurrency trackers and others. In order for widgets to be useful, the data always must be kept up-to-date, so the app should obtain a fresh dataset once in a while.
Preserving widgets with recent data is unfortunately not as straightforward as it appears to be, so in this article we will take a look at how to implement it correctly and avoid some headaches.
Scheduling periodic widget updates
Let’s imagine that we want to build a simple weather forecast app containing a widget with a forecast for some user-defined location. Keep in mind that obtaining user’s location data from a widget is also not as simple as it seems, so if you need it for your use-case, refer to this article for more information.
Approach 1: updatePeriodMillis parameter
Official documentation about app widgets refers to a parameter called updatePeriodMillis
that should be specified for each widget within AppWidgetProviderInfo
XML file. This is the easiest way to refresh your widget periodically – just specify how often the widget should be updated and the system will handle the rest. To be exact, this parameter specifies how often the onUpdate()
callback inside your AppWidgetProvider
will be called.
However, there are unfortunately some drawbacks connected to this method. First, you cannot set the update period to less than 30 minutes and if you do, the system will ignore your value and use the 30 minutes interval as allowed minimum. This is not a big deal for most applications and it helps to preserve the battery life of the device, so unless you have a good reason, you should stick with a 30 minutes or longer interval anyways. But if you really need a shorter period between data updates, this method is not suitable for it.
Second, and much bigger issue, is that the widget stops refreshing after the app is updated. This means that when a user installs your app and adds a widget to his home screen, everything will work like a charm, but when the first update of the app is installed, widgets get stuck. After an app update occurs, onUpdate()
is called for each active AppWidgetProvider
once, but stops being called periodically by the system. This means that if you rely just on onUpdate()
being periodically called by Android, your widget will never get updated again and it will be forever stuck with outdated forecasts, news etc.
This is the reason why some developers use a different approach when planning widget updates and we will explore it in the next sections.
Approach 2: WorkManager library
WorkManager is a library created by Google to help with running background tasks. It can do some really cool tricks, like running the task only if the device is connected to the Internet, has enough battery level, it can also automatically retry the task if it failed for some reason etc. So it seems that this may be a great solution for updating widgets.
Unfortunately it isn’t. Refer to the great article WorkManager, App Widgets, and the Cost of Side Effects for more details. In short: there is a bug in this library that prevents scheduling tasks from onUpdate()
callback, therefore if you do, you end up with an infinite loop. This means that Work Manager is still a great library for scheduling background tasks, but not if you want to use it for updating the app widgets. It can be used for some specific use cases, for example when you need to update a widget once after some user-initiated action in the app, but it is not suitable for scheduling typical periodic updates.
Approach 3: Planning updates manually by AlarmManager
Fortunately Android provides us a lower level API within the AlarmManager
class that allows us to overcome issues in approaches mentioned above. With great power comes great responsibility though, so we need to write a bit more code to correctly plan updates that are run when they should and also cancel those that are no longer needed, such as when a widget is removed from the home screen.
The idea behind planning manual updates is the following:
When
onUpdate()
is called, plan nextonUpdate()
call for all widgets within currentAppWidgetProvider
- This covers cases when a widget is added to the home screen or periodic update is called.
- Essentially this means that from every
onUpdate()
call, an alarm that runsonUpdate()
again is scheduled
When widget is removed, reschedule new alarm that does not contain ID of removed widget
If all widgets belonging to this
AppWidgetProvider
are removed, cancel the update alarm completely
Let’s convert it into actual code:
class CustomWidgetProvider : AppWidgetProvider() {
override fun onUpdate(/* … */, context: Context, appWidgetIds: IntArray) {
appWidgetIds.forEach { appWidgetId ->
onUpdateWidget(context, appWidgetId)
}
scheduleUpdates(context)
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
// reschedule update alarm so it does not include ID of currently removed widget
scheduleUpdates(context)
}
override fun onDisabled(context: Context) {
cancelUpdates(context)
}
private fun onUpdateWidget(context: Context, appWidgetId: Int) {
// update widget content
}
private fun getActiveWidgetIds(context: Context): IntArray {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, this::class.java)
// return ID of all active widgets within this AppWidgetProvider
return appWidgetManager.getAppWidgetIds(componentName)
}
private fun scheduleUpdates(context: Context) {
val activeWidgetIds = getActiveWidgetIds(context)
if (activeWidgetIds.isNotEmpty()) {
val nextUpdate = ZonedDateTime.now() + WIDGET_UPDATE_INTERVAL
val pendingIntent = getUpdatePendingIntent(context)
context.alarmManager.set(
AlarmManager.RTC_WAKEUP,
nextUpdate.toInstant().toEpochMilli(), // alarm time in millis since 1970-01-01 UTC
pendingIntent
)
}
}
private fun getUpdatePendingIntent(context: Context): PendingIntent {
val widgetClass = this::class.java
val widgetIds = getActiveWidgetIds(context)
val updateIntent = Intent(context, widgetClass)
.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds)
val requestCode = widgetClass.name.hashCode()
val flags = PendingIntent.FLAG_CANCEL_CURRENT or
PendingIntent.FLAG_IMMUTABLE
return PendingIntent.getBroadcast(context, requestCode, updateIntent, flags)
}
private val Context.alarmManager: AlarmManager
get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
companion object {
private val WIDGET_UPDATE_INTERVAL = Duration.ofMinutes(30)
}
}
As you can see, there is some extra work needed when compared to previous solutions, but you now have much more control over planning the updates and they are more reliable. While alarms are cleared as well after app update, this is not an issue because one onUpdate()
call is fired and we always plan the update alarm inside the callback, so the widget continues to work as expected.
Another benefit is that you can specify whatever update interval you want (so even shorter than 30 mins), but keep in mind that alarms set by alarmManager.set()
are inexact, which means that the system can postpone its execution to preserve battery, which will definitely happen when phone is in deep sleep (Doze mode). This can be solved with alarmManager.setExactAndAllowWhileIdle()
, but usage of this API requires declaring SCHEDULE_EXACT_ALARM
permission and user can revoke this permission at any time in app settings. So unless your widget needs to be updated at the very exact time, just stick with inexact alarms.
Battery savers
In the previous section, we’ve shown how to schedule widget updates reliably. However, even the mentioned solution won’t work in some cases. Android is used by a very broad scale of devices, each system version handles background processes differently and many manufacturers incorporate their custom battery & memory savers that can kill your app at any time. This is unfortunately much harder to solve and no universal solution exists.
Refer to Don’t Kill My App website that explains details about these battery savers and provides steps on how to mitigate their negative effects either by user or developer.
App widgets: Conclusion
Android app widgets are definitely not very developer friendly, as they work in the background and with almost every new Android version, there are new restrictions on background tasks imposed, so refer to the official changelogs to keep up with requirements of latest Android versions. However as of Android 12, this approach is sufficient to meet most of the use cases regarding updating app widgets.