-
Notifications
You must be signed in to change notification settings - Fork 8
Widget Update Chain
这页讲 Sleepy 桌面组件如何被通知刷新:三条触发路径、APPWIDGET_UPDATE 广播怎么遍历 10 个 receiver、厂商冻结窗口下同步广播为什么可靠,以及组件不刷新时怎么排查。
三条路径最后都汇到同一个函数:app/src/main/java/com/lingion/sleepy/widget/WidgetUpdater.kt:96 的 suspend fun notifyDataChanged(context: Context)。它做两件事:
- 对全部 10 个已注册 widget receiver 同步广播
android.appwidget.action.APPWIDGET_UPDATE,各 receiver 在onUpdate里调awm.updateAppWidget(id, views)完成重绘。 - 排一个到下一个本地午夜的 WorkManager 单次任务,保证跨 00:00 后立刻显示新的一天(
WidgetUpdater.kt:59)。
10 个 receiver 来自 ALL_WIDGET_VARIANTS(app/src/main/java/com/lingion/sleepy/widget/WidgetVariantInfo.kt:19):WeekGrid / Today / WeekList / WeekView / TwoDay 各有普通与小尺寸两个变体。这个列表是唯一真源,新增 widget 变体只需在此加一条,刷新广播自动覆盖。
课表数据的一切写操作集中在 app/src/main/java/com/lingion/sleepy/data/repository/ScheduleRepository.kt:326 的 onDataChanged()。它先调 WidgetUpdater.notifyDataChanged(app) 刷桌面组件,再调 notificationScheduler.scheduleAll() 重排课前提醒与流体云(ScheduleRepository.kt:328-333),两件事一起做,widget 与通知不会各刷各的。
调 onDataChanged() 的写方法覆盖 18 处:课表增删改与切表(ScheduleRepository.kt:87、120、134、142)、单课与批量导入(ScheduleRepository.kt:165、174、185、193)、组编辑与删课(ScheduleRepository.kt:200、214、218、226、236)、行级 diff 保存(ScheduleRepository.kt:288)、覆盖式导入(ScheduleRepository.kt:300)、撤回恢复(ScheduleRepository.kt:58)。
两个例外:
-
insertTable单独建表不触发刷新(ScheduleRepository.kt:75-82);建完空表无课程变化,ViewModel 层的createEmptyTable建完会补一次手动刷新(ScheduleViewModel.kt:202)。 - 有一层 UI 触发不经过 Repository:
ScheduleViewModel.loadCourses收到 Room Flow 新数据时直接调notifyDataChanged(app/src/main/java/com/lingion/sleepy/ui/screen/schedule/ScheduleViewModel.kt:111),切表selectTable也单独刷(ScheduleViewModel.kt:128)。
WidgetUpdater.schedule()(WidgetUpdater.kt:73)排一个 15 分钟一次的周期任务:REPEAT_MINUTES = 15L,首次延迟 3 分钟(WidgetUpdater.kt:78),唯一名 sleepy_widget_update + KEEP 策略,重复注册不叠加。挂载点在 Application.onCreate(app/src/main/java/com/lingion/sleepy/SleepyApp.kt:62),每次冷启动都确认一次。
选 WorkManager 有一个硬原因:系统自带的 updatePeriodMillis 下限 30 分钟,WidgetUpdateWorker.kt:9 注释明写「用 WorkManager 突破这个限制」。10 份 widget provider XML 全部把 android:updatePeriodMillis 设为 0(app/src/main/res/xml/today_widget_info.xml:11 等 10 个文件),系统级定时刷新是关闭状态。
Worker 本体只有几行(app/src/main/java/com/lingion/sleepy/widget/WidgetUpdateWorker.kt:17-21):调 notifyDataChanged,再补一句 ensureActiveFluidCloud() —— 应用长时间不在前台时,如果当前正处在某节课的课前提醒窗口,流体云会被补起。
这条路径同时承担跨天保险:15 分钟周期在 doze 下凌晨可能迟到,所以每次 notifyDataChanged 都会把单次任务重排到下个午夜(唯一名 sleepy_widget_midnight_update + REPLACE,幂等),午夜 worker 执行时又走一遍 notifyDataChanged,任务自我延续。时区变化会打断挂钟对准,由下一次任意刷新重设(WidgetUpdater.kt:33-37)。
SleepyApp 覆写 onConfigurationChanged(SleepyApp.kt:82-94):取 uiMode and UI_MODE_NIGHT_MASK 与上次记录比较,变了才起协程调 notifyDataChanged。屏幕旋转等无谓变化被这个去重挡掉,只刷一次。
主题变化因此有两条进入刷新流程的路:
- 系统运行时切深浅色 →
SleepyApp.onConfigurationChanged(SleepyApp.kt:82) →notifyDataChanged。Android 原生行为是 configuration change 时系统会向所有 widget 重发 APPWIDGET_UPDATE(SleepyApp.kt:72注释),这里再主动广播一次作为保险。MainActivity在 manifest 声明了android:configChanges="uiMode"(app/src/main/AndroidManifest.xml:39),切主题不会重建 Activity。 - 应用内手动切主题/换配色 →
MainActivity.kt:124的onThemeModeChange回调,在lifecycleScope里调notifyDataChanged(MainActivity.kt:130)。
广播落到 receiver 后,渲染走 WidgetContent 的取色入口按当前主题重新画 bitmap,桌面组件随之变色。渲染细节见 桌面组件渲染技术。
notifyDataChanged 在 Dispatchers.IO 里遍历 remoteViewsReceiverClasses(= ALL_WIDGET_VARIANTS.map { it.receiverClass },WidgetUpdater.kt:46-47),对每个 receiver(WidgetUpdater.kt:105-121):
-
ComponentName(context, receiver)定位到具体 receiver。 -
awm.getAppWidgetIds(component)取该 receiver 下所有已放置实例;为空直接跳过,不发广播。 - 构造显式 Intent:action
android.appwidget.action.APPWIDGET_UPDATE、EXTRA_APPWIDGET_IDS带上 id 列表、component指向该 receiver。 -
context.sendBroadcast(intent)。显式 Intent 只投递给指定的那一个 receiver,不广播全系统。
每个 receiver 单独 try/catch,一个失败不影响其余。日志 tag 为 WidgetUpdater,格式 <ReceiverSimpleName> ids=[...](WidgetUpdater.kt:111),排障时在 logcat 里搜这个 tag 就能看到广播发到了谁。
receiver 侧的承接:以 TodayWidgetReceiver 为例,onUpdate 用 goAsync() 起后台协程逐 id 渲染推送(app/src/main/java/com/lingion/sleepy/widget/TodayWidget.kt:64-74);单个 id 渲染失败保留上一份有效 RemoteViews,桌面继续显示旧内容,只记日志(TodayWidget.kt:58-61)。
Glance 时代的旧实现走 provideGlance 异步 SessionWorker:进程一旦被 OPPO OplusHansManager 冻结,SessionWorker 被冻结,RemoteViews 永远生成不出来,桌面卡在加载布局,也不跟随主题(TodayWidget.kt:24-27 注释记录了这条成因)。
现在的实现是同步 RemoteViews AppWidgetProvider:从 sendBroadcast 到 onUpdate 完成 updateAppWidget 全程在主进程内、几百毫秒内跑完,在冻结窗口(约 5 秒,WidgetUpdater.kt:104 注释)内就已把视图推给启动器,冻结不再构成威胁。v1.0.29 起 Today/WeekList/TwoDay 也从 Glance 移植到这条同步路径(WidgetUpdater.kt:102-104 注释),Glance 层已删除,10 个 receiver 全部同路径。
按顺序排查:
- 等一个周期。15 分钟周期任务在 doze 下可能更晚触发。装了 widget 但数据一直不动,先等 15 分钟再判断。
-
手动触发一次。进「我的」页,点「刷新所有桌面组件」按钮(
app/src/main/java/com/lingion/sleepy/ui/screen/mine/MineScreen.kt:134),成功会弹出「已刷新全部桌面组件」(app/src/main/res/values-zh-rCN/strings.xml:280)。弹了 toast 仍不更新 → 问题在 receiver 渲染或启动器侧;没弹 → 广播链本身没跑起来。 -
查电池优化。OPPO/vivo/小米等厂商的后台限制可能杀掉 WorkManager 周期任务,路径二失效但手动路径(前台触发)仍可用。把 Sleepy 移出电池优化白名单后重启一次 app,让
Application.onCreate重新注册周期任务(SleepyApp.kt:62)。 -
跨 0 点日期没变。午夜任务是单次 WorkManager 任务,强杀进程会清掉它;重开一次 app 即可重建(冷启动的
notifyDataChanged会顺手重排,SleepyApp.kt:56-58)。 -
改时区/改系统时间后日期错乱。午夜任务按本地挂钟排,时区跳变会打断对准;手动刷一次,下一次刷新会重设(
WidgetUpdater.kt:33-37)。 -
logcat 定位。搜 tag
WidgetUpdater:有<Receiver> ids=[...]输出说明广播已发出,问题在 receiver 内部渲染(再搜render failed);没有任何输出说明notifyDataChanged未被调用或该 receiver 下没有已放置实例。
Sleepy Wiki
入门
界面与视图
课程管理
导入导出
小组件与提醒
数据与设置
项目
社区