fix: force widget re-render on app launch to recover Glance state#25
Conversation
Glance's internal appWidgetId-to-class mapping can become stale after a force-stop or app update, causing content bleed between widgets. Calling updateAll for both widget types on every app launch re-establishes the correct mapping. Mitigates #23
Reviewer's guide (collapsed on small PRs)Reviewer's GuideOn app launch, MainActivity now forces both Glance-based widgets (WeatherWidget and AlarmWidget) to re-render via updateAll before scheduling background work, ensuring Glance’s internal widget-to-class state mapping is corrected after force-stop or app updates. Sequence diagram for widget re-render on app launchsequenceDiagram
actor User
participant MainActivity
participant LifecycleScope
participant WeatherWidget
participant AlarmWidget
participant GlanceRuntime
participant DataStore
participant WorkScheduler
User ->> MainActivity: launch app
MainActivity ->> LifecycleScope: launch(Dispatchers.IO)
rect rgb(230, 245, 255)
LifecycleScope ->> WeatherWidget: updateAll(applicationContext)
WeatherWidget ->> GlanceRuntime: request re-render for all WeatherWidget instances
GlanceRuntime -->> WeatherWidget: rebuild state mapping for WeatherWidget
LifecycleScope ->> AlarmWidget: updateAll(applicationContext)
AlarmWidget ->> GlanceRuntime: request re-render for all AlarmWidget instances
GlanceRuntime -->> AlarmWidget: rebuild state mapping for AlarmWidget
end
LifecycleScope ->> DataStore: read preferences
DataStore -->> LifecycleScope: prefs with LOCATION_LAT, LOCATION_LON
alt location set in prefs
LifecycleScope ->> WorkScheduler: schedule background work
else location not set
LifecycleScope -->> MainActivity: skip scheduling
end
MainActivity -->> User: app UI ready, widgets corrected on home screen
Class diagram for MainActivity widget update integrationclassDiagram
class MainActivity {
+onCreate(savedInstanceState: Bundle): void
+lifecycleScope: LifecycleCoroutineScope
}
class WeatherWidget {
+updateAll(context: Context): void
}
class AlarmWidget {
+updateAll(context: Context): void
}
class WorkScheduler {
+scheduleWork(lat: Double, lon: Double): void
}
class WeatherDataStore {
<<object>>
+LOCATION_LAT: PreferencesKey
+LOCATION_LON: PreferencesKey
}
class DataStore {
+data: Flow
}
MainActivity --> WeatherWidget : uses
MainActivity --> AlarmWidget : uses
MainActivity --> WorkScheduler : uses
MainActivity --> DataStore : reads
DataStore --> WeatherDataStore : uses keys for location
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- Consider limiting the unconditional
WeatherWidget().updateAllandAlarmWidget().updateAllcalls (e.g., by checking whether any instances exist or whether this is the first launch after an upgrade/force-stop) to avoid doing potentially expensive widget re-renders on every app launch. - You might want to extract the self-healing Glance re-render logic into a dedicated helper or manager (e.g.,
WidgetStateRecovery.repairIfNeeded(context)) so this behavior is centralized and easier to reuse or evolve if more widgets are added.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider limiting the unconditional `WeatherWidget().updateAll` and `AlarmWidget().updateAll` calls (e.g., by checking whether any instances exist or whether this is the first launch after an upgrade/force-stop) to avoid doing potentially expensive widget re-renders on every app launch.
- You might want to extract the self-healing Glance re-render logic into a dedicated helper or manager (e.g., `WidgetStateRecovery.repairIfNeeded(context)`) so this behavior is centralized and easier to reuse or evolve if more widgets are added.
## Individual Comments
### Comment 1
<location path="app/src/main/java/com/wassupluke/widgets/ui/MainActivity.kt" line_range="62-63" />
<code_context>
lifecycleScope.launch(Dispatchers.IO) {
+ // Force both widgets to re-render, re-establishing correct Glance
+ // state mapping in case it drifted after a force-stop or update
+ WeatherWidget().updateAll(applicationContext)
+ AlarmWidget().updateAll(applicationContext)
+
+ // Schedule WorkManager (if location is already set)
</code_context>
<issue_to_address>
**issue (bug_risk):** Consider isolating widget updates from the WorkManager scheduling so a failure in `updateAll` doesn't block scheduling.
Because both widget updates run in the same coroutine as scheduling, any exception from `updateAll` will cancel the coroutine and prevent scheduling from running. To avoid missing periodic updates, consider wrapping each `updateAll` in its own try/catch or moving the scheduling into a separate coroutine.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| WeatherWidget().updateAll(applicationContext) | ||
| AlarmWidget().updateAll(applicationContext) |
There was a problem hiding this comment.
issue (bug_risk): Consider isolating widget updates from the WorkManager scheduling so a failure in updateAll doesn't block scheduling.
Because both widget updates run in the same coroutine as scheduling, any exception from updateAll will cancel the coroutine and prevent scheduling from running. To avoid missing periodic updates, consider wrapping each updateAll in its own try/catch or moving the scheduling into a separate coroutine.
Summary
appWidgetId→class mapping can become stale after force-stop or app update, causing one widget to render the other's content and tap targetsupdateAllfor bothWeatherWidgetandAlarmWidgeton every app launch, forcing Glance to re-render with the correctprovideGlanceand re-establish its state mappingMitigates #23
Test plan
Summary by Sourcery
Bug Fixes: