4 September 2026 · 5 min read

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.

The path from a data change to a redrawn widget Five boxes in sequence: data changes in the app's store; a WorkManager job or broadcast wakes the app; Glance recomposes the widget content; new RemoteViews are built and sent; the launcher redraws. Arrows between them are labelled with the kind of delay: scheduling, deferral, recomposition, transfer. Every arrow is a delay, and two can be deferred Data changes in the store Worker wakes may be deferred Recompose Glance content RemoteViews built and sent Launcher redraws Until the last box, the launcher shows the previous picture, however old it is. the second box is where the system decides whether your app runs at all
Illustrative: the update chain as the Glance documentation describes it, drawn as a sequence.

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.

Minimum interval between widget updates, by mechanism Horizontal bars in minutes: updatePeriodMillis, 30; periodic WorkManager, 15; exact alarm, no fixed floor but needs a permission and costs battery; immediate update while the app is awake, zero. The first two are the mechanisms available while the app is asleep. What "at most N minutes old" can actually be Documented minimum interval between updates, minutes updatePeriodMillis 30 Periodic WorkManager 15, and may be deferred Exact alarm no floor; needs a permission, drains battery App already awake 0, on demand While the app is asleep, the honest promise is thirty minutes, or fifteen with luck.
Source: the Android documentation for Glance widgets, periodic work and alarms, September 2026.

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 snapshot contract: three promises and the mechanism under each Three columns. Promise one: what is shown is at most N minutes old, kept by choosing content that is true for N minutes and a periodic worker at that interval. Promise two: a tap lands on a screen that is fresh, kept by making the tap open the app and reload rather than act directly. Promise three: the widget never shows an error state, kept by rendering the last good snapshot and a timestamp rather than a failure. What the picture promises At most N minutes old show content that stays true for N minutes periodic worker at N, immediate update when the app is open N is 15 or 30, not 1 A tap lands somewhere fresh the tap opens the app, which reloads before acting on anything no destructive action from a stale number a link, not a control Never an error state keep the last good snapshot and show it add a small "as of" time when it is old failed refresh: no change Choose the mechanism to keep the promises, not the promises to flatter the mechanism.
Illustrative: the contract as I write it for a widget; the mechanisms named are the ones the documentation offers.

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.

AndroidJetpack GlanceUX
All writing

Written by Mohd Shayan

Get new posts by email

Occasional essays on engineering, AI, and building for the people technology leaves behind.

One email per new post. Unsubscribe any time.

Subscribe with RSS