Compose for widgets, RemoteViews underneath

Calendula’s home-screen widgets are written in Jetpack Glance — the Compose-style API for widgets. You declare a GlanceAppWidget, write something that looks like a composable, and attach behaviour with familiar-feeling modifiers. It’s a genuine relief compared to hand-assembling widget layouts.

But Glance isn’t a new widget runtime. It’s a translation layer: your composition is compiled down to RemoteViews — a serialized description of a layout that another process (the launcher) inflates and renders. Your app isn’t running while the widget is on screen, and it never gets the touch events. Glance hides that, but it can’t repeal it. Three Calendula widget bugs in a row were really the same lesson: the RemoteViews constraints reach up through the Compose gloss and bite anyway.

There’s no click handler, only an action

In a real Compose UI you’d write Modifier.clickable { doThing() } and doThing runs in your process. In Glance you write GlanceModifier.clickable(actionStartActivity<…>()) — and that difference is the whole story. There’s no lambda that runs on tap, because there’s no code of yours running on the far side. Glance only lets you attach an actionactionStartActivity to launch something, actionRunCallback to fire a registered callback — and those compile down to the PendingIntents RemoteViews has always required. Taps don’t call a function; they launch something.

That reframes every “make X tappable” request. When the month widget’s arrows and header did nothing (#18), it wasn’t a broken handler — there was no handler to break, and no action wired to the view either, so the launcher had nothing to fire. The fix isn’t “handle the tap,” it’s “attach the action”: one that advances the month, one that opens the app in month view. Same shape in #20 — the agenda widget’s header should open your default view, again a missing action, not a bug in one.

And because every day cell needs to open its own date, each one carries its own action. Glance’s clickable makes that look like an ordinary per-item modifier, but underneath it’s still one addressed intent per cell — a month grid is a grid of them. An earlier round (#2) fixed exactly this: the month grid wasn’t interactive at all until each day was made to launch itself.

The far side has to be told what “back” means

Because a tap launches an intent rather than navigating, the app has to reconstruct context on arrival. Tapping a day in the agenda widget and tapping a day in the month widget both open a day — but where should Back take you? The widget knows; the freshly-started activity does not, unless the action says so.

So actionStartActivity carries not just “open this day” but “you came from the agenda context,” and the app rebuilds a back stack from that: day → agenda → your default view, or day → month → default. It looks like normal navigation to the user. Under the hood it’s the app trusting a breadcrumb the widget packed into the launch, because a widget can’t hand over a live navigation state — Glance composition or not, all it can serialize is data.

“Upcoming” has to actually mean upcoming

The last one wasn’t about interaction. The agenda widget is titled Upcoming, yet it listed every event of the day, finished ones included (#12). In a normal Compose list you’d just filter as the list recomposes. A widget can’t — what the launcher renders is baked in when the composition is snapshotted to RemoteViews, not recomputed as you scroll. So “don’t show past events” becomes a property of the data you build before Glance serializes it, exposed as a PastEventDisplay preference: hide finished events outright, or dim them in place for people who still want them visible.

The lesson

Glance is a real improvement — declaring widgets in Compose beats the old ceremony. But it’s a nicer handle on the same box. There’s still no process of yours, still no live view tree, still only data you snapshot and actions you pre-address, rendered by someone else. Once I stopped thinking “how do I handle this tap” and started thinking “what action should this fire, and what does the far side need to know,” the three bugs stopped looking separate. A widget can’t do anything. It can only describe what should happen — and Compose syntax doesn’t change that, it just makes the describing pleasant.