如何在Android 8.0 - Oreo - API 26中正确更新小部件

35
假设我有一个小部件,其targetSDKVersion设置为26。这个小部件需要花费100毫秒到10秒钟来更新,大多数情况下少于1秒。在Android O之前,如果AppWidgetProvider上调用了onUpdate(),我可以启动后台服务来更新此小部件。然而,如果尝试该行为,则Android O会返回IllegalStateException异常。显然,启动前台服务似乎是一种极端措施,因为99%的时间内都会在10秒内完成。

可能的解决方案:

  • 启动前台服务以更新小部件。通过通知打扰用户,但将在10秒钟内消失。
  • 使用JobScheduler尽快安排作业。您的小部件可能会在一段时间内得到更新,也可能不会得到更新。
  • 尝试在广播接收器中执行工作。阻止其他应用程序的UI线程。恶心。
  • 尝试在小部件接收器中工作。阻止其他应用程序的UI线程。恶心。
  • 滥用GCM以获取运行后台服务。很麻烦,感觉像是黑客。

我个人不喜欢上述任何一种解决方案。希望我漏掉了什么更好的解决方法。

(更加令人沮丧的是,我的应用程序已经被系统加载到内存中通过调用onUpdate()。我不明白为什么在将我的应用程序加载到内存并调用onUpdate()之后,不允许我的应用程序有1秒的时间在UI线程之外更新小部件能够节省任何电池寿命。)


1
你找到更简洁的方法了吗?我也遇到了IllegalStateException。 - SAIR
2个回答

11
您没有指明更新触发机制是什么。您似乎关心的是延迟("您的小部件可能会有一段时间没有更新"),因此我将假设您的关注点与用户与应用程序小部件的交互有关,例如点击按钮。

使用JobScheduler尽快调度任务。您的小部件可能会有一段时间没有更新。

这是"使用JobIntentService"的变体,据我所知,这是这种情况下的推荐解决方案。
其他选项包括:
  • 使用带有PendingIntentgetForegroundService()。通过这样做,您有效地“承诺”您的服务将在ANR时间范围内调用startForeground()。如果工作时间超过几秒钟,则调用startForeground()以确保Android不会发脾气。这应该最小化前台通知出现的次数。如果用户点击了一个按钮,而您仍在忙于进行工作几秒钟后,您可能希望显示通知或以某种方式让用户知道他们要求的内容仍在进行中。

  • BroadcastReceiver上使用goAsync(),在不占用主应用程序线程的情况下,在接收器的上下文中执行工作。我没有尝试过在Android 8.0+上使用此功能,所以您的情况可能有所不同。


1
@Justin: "我确实指的是 onUpdate() 调用了 AppWidgetProvider 触发机制" -- 顺便说一下,这并不是我所指的触发机制。您提到的事件(例如重启)以及如果使用 updatePeriodMillis 则时间流逝都是触发器。此外,您完全可以创建一个与应用程序小部件中的按钮相关联的 PendingIntent ,最终路由到 onUpdate() 。如果您关心的是 updatePeriodMillis ,则可以放弃它并使用 JobScheduler 进行定期更新。对于其他触发器,我建议尝试我在回答中概述的技术。 - CommonsWare
2
我的小部件有时候在迁移到JobIntentService后无法显示更新的响应。通过添加日志信息,我发现在某些情况下,作业被延迟了7分钟,过去七分钟的所有小部件更新都会一次性显示出来。我已经接受了答案,但我必须继续尝试其他方法,因为JobIntentService显然不适用于与UI相关的任何内容。 - Justin
4
在广播接收器中使用goAsync()进行初始测试效果良好! - Justin
2
@Justin 你最终选择使用goAsync了吗? - AdamWardVGP
我相信使用JobScheduler是实现这一目标的完美方式,可以通过使用JobInfo.Builder.setOverrideDeadline(long)来确保作业被调度。更多信息请参见:https://developer.android.com/reference/android/app/job/JobInfo.Builder.html#setOverrideDeadline(long)。 - SAIR
显示剩余3条评论

6
您可以使用WorkManager来更新小部件。在API 14+的设备上使用WorkManager。您需要像这样覆盖fun onReceive(context: Context?, intent: Intent?):
val ACTION_AUTO_UPDATE : String = "AUTO_UPDATE";

override fun onReceive(context: Context?, intent: Intent?) {
    super.onReceive(context, intent)
    if(intent?.action.equals(ACTION_AUTO_UPDATE))
    {
        val appWidgetManager = AppWidgetManager.getInstance(context)
        val thisAppWidgetComponentName = ComponentName(context!!.getPackageName(), javaClass.name)
        val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidgetComponentName)
        for (appWidgetId in appWidgetIds) {
            // update widget
        }
    }
}

你需要创建一个PeriodicWorkRequest。这个请求用来重复执行工作。周期性工作的最小间隔为15分钟。当小部件被启用时,我们将周期性工作排队执行:

override fun onEnabled(context: Context) {
    val periodicWorkRequest = PeriodicWorkRequest.Builder(YourWorker::class.java, 15, TimeUnit.MINUTES).build()
    WorkManager.getInstance(context).enqueueUniquePeriodicWork("YourWorker", ExistingPeriodicWorkPolicy.REPLACE,periodicWorkRequest)
}

当小部件被禁用时,请取消它:

override fun onDisabled(context: Context) {
    WorkManager.getInstance(context).cancelAllWork()
}

最后我们创建一个工作者类:

class YourWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
var context : Context? = null

init {
    context = ctx
}

override fun doWork(): Result {
    val alarmIntent = Intent(context, YourWidget::class.java)
    alarmIntent.action = YourWidget().ACTION_AUTO_UPDATE
    context?.sendBroadcast(alarmIntent)
    return Result.success()
}

如果您想使用WorkerManager,需要在build.gradle中添加implementation 'androidx.work:work-runtime:2.3.1'。您可以在此处找到示例:链接

  1. 这个示例与AppWidgetProviders中的WorkManager无关。
  2. 在AppWidgetProvider中使用WorkManager.getInstance(context)会导致某些设备上出现IllegalStateException。因为context可能不是应用程序的context,而且WorkManager可能没有在该context中初始化。
  3. onEnabled/onDisabled()不能帮助更新onReceive()中的小部件。
- Alex
在YourWorker类中使用getApplicationContext()而不是存储第二个context字段。 - Prilaga

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接