A widget is a photograph of your app
A home-screen widget is not a running view; it is a picture the launcher shows until you send a new one. Design for the age of the picture, not for the code that drew it.
The mental model most people bring to a home-screen widget is that it is a small window onto the app, a live view that happens to sit on the launcher. It is not. A widget is a picture. The app draws it, hands the drawing to the launcher, and the launcher shows that drawing until the app sends a new one, which may be minutes later, may be half an hour later, and may not happen at all if the system decides the phone is better off asleep. Jetpack Glance makes drawing the picture pleasant, with composables instead of RemoteViews by hand, and that pleasantness hides the fact that the result is still a snapshot. Designing a widget well means designing for the age of the snapshot.
What the launcher actually holds
Under the composable surface, a Glance widget is translated into RemoteViews and sent to the launcher process; the Glance documentation is explicit that to update the content, Glance must recreate the RemoteViews and send them again. Nothing recomposes on the launcher. A state change in the app does not reach the widget until something in the app calls update, and that something has to be running. The sequence, when it works, is: data changes, a worker or broadcast wakes the app, the widget's content is recomposed, new RemoteViews are built, the launcher redraws. Each arrow in that chain has a delay, and the first two can be deferred by the system.
How old the picture can be
The mechanisms that can trigger the second box have minimum intervals, and they are documented. The widget's own updatePeriodMillis setting delivers updates at most once every thirty minutes, whatever smaller value you write. A periodic WorkManager request has a minimum repeat interval of fifteen minutes, and the same page is careful to say that the exact time the worker runs also depends on constraints and system optimisations, and that a run can be delayed or skipped. Alarms can fire more often, but inexact alarms are batched and deferred in Doze, and exact alarms on Android 12 and later require a permission that the user can revoke. An immediate update, with no minimum at all, is available only when the app is already awake: the user is in it, or has just tapped the widget, or a push message has arrived.
The documentation's own advice is to avoid updating every minute when the app is not awake, because it drains the battery, and that advice is right. The consequence for design is that a widget's picture, while the app sleeps, is somewhere between fifteen and thirty minutes old on a good day and older when the system is conserving power. A widget that shows a number which changes every minute is showing a lie most of the time, drawn beautifully.
The snapshot contract
So the design question is not "how do I keep the widget fresh" but "what does the picture promise, and how do I keep the promise given the mechanisms I have". I write that down as three promises, and choose the update mechanism to fit them.
The first promise is about age: what is shown is at most N minutes old, and N is a number the mechanisms can keep, which means fifteen or thirty while the app sleeps, and the content shown has to be something that is still true after that long. For the habit tracker I built, that means today's habits and whether each is done, which changes a few times a day, rather than a countdown or a streak-at-risk timer that changes by the minute. The mechanism under the promise is a periodic worker at N plus an immediate update whenever the app is in the foreground, so that the common case, the user marks a habit done and goes back to the home screen, is instant.
The second promise is about the tap: it lands on a screen that is fresh. The temptation with a widget is to make it a control, with a tick box that marks the habit done without opening the app. That is fine when the picture is current and wrong when it is not, because the user is acting on a state that may have changed, and a destructive action from a stale snapshot is the worst outcome a widget can produce. So the tap opens the app to the relevant screen, the screen reloads, and the action happens there. The picture is a link, not a control. Where an in-place action is genuinely worth it, it is safe only for idempotent changes, and the widget must update itself immediately afterwards from the app's own state rather than assuming the tap succeeded.
The third promise is about failure: the widget never shows an error state. A refresh that fails, because the worker was deferred or the data was unavailable, leaves the last good snapshot in place; the launcher was already showing it, and replacing it with a message about a failed update is worse in every way. What the widget may do is show a small "as of" time once the snapshot is older than N, so that the user can tell, and so that the promise of age is visible rather than implied.
The contract, written out
For the habit tracker, the contract reads like this. The widget shows today's habits and which are done, and it promises that the picture is at most thirty minutes old while the app is asleep and current whenever the app has just been used. It keeps the first half of that promise with a periodic worker at the documented floor and the second half with an update call in the app's lifecycle, fired when the user leaves the app, which is the moment they are most likely to look at the home screen. Tapping any habit opens the app on today's list with that habit in view; marking it done happens there, in a screen that has just read the store. The widget's own content is written so that it is still true after thirty minutes: a habit that was done at nine is still done at nine thirty, and a habit that was not done is still not done unless the user did it, in which case they did it in the app and the widget was updated on the way out.
What the first version had, and the contract removed, was a countdown to the end of the day. It looked good and it was wrong most of the time, because the launcher showed a countdown frozen at whatever minute the last snapshot was taken, and a frozen countdown is worse than none. The contract's first promise ruled it out, and the replacement, a plain count of habits remaining today, is true for as long as the snapshot lives.
Designing for the age
The pattern generalises past Glance and past Android. Anything that renders a snapshot into a surface it does not control, a lock-screen complication, a status bar item, an email digest, a dashboard tile refreshed by a cron job, is a photograph of the system, and the same three questions apply: how old may it be, what happens when someone acts on it, and what does it show when the refresh fails. The composable that draws it is the easy part. The contract about its age is the design, and it is worth writing down before the first line of the widget, because the mechanisms do not bend to a promise made afterwards.
Get new posts by email
Occasional essays on engineering, AI, and building for the people technology leaves behind.
Subscribe with RSS