Add six draft posts drawn from Calendula issues
Technical, journey, and philosophy posts sourced from the Calendula issue threads, reviewed for source-accuracy, standards, ethics, and voice. All draft:true with staggered future pubDates; they stay hidden in prod until approved (draft:false) and due. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
92
src/content/blog/widgets-remoteviews.md
Normal file
92
src/content/blog/widgets-remoteviews.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: Compose for widgets, RemoteViews underneath
|
||||
description: >-
|
||||
Jetpack Glance lets you write home-screen widgets in Compose. But it compiles
|
||||
down to RemoteViews — so the old constraints still bite through the nice API.
|
||||
Three Calendula bugs that proved it.
|
||||
pubDate: 2026-07-02
|
||||
tags: [android, architecture, calendula]
|
||||
draft: true
|
||||
---
|
||||
|
||||
Calendula's home-screen widgets are written in
|
||||
[Jetpack Glance](https://developer.android.com/develop/ui/compose/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`](https://developer.android.com/reference/android/widget/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 **action** —
|
||||
`actionStartActivity` to launch something, `actionRunCallback` to fire a
|
||||
registered callback — and those compile down to the `PendingIntent`s 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.
|
||||
|
||||
[#2]: https://codeberg.org/jlmakiola/calendula/issues/2
|
||||
[#12]: https://codeberg.org/jlmakiola/calendula/issues/12
|
||||
[#18]: https://codeberg.org/jlmakiola/calendula/issues/18
|
||||
[#20]: https://codeberg.org/jlmakiola/calendula/issues/20
|
||||
Reference in New Issue
Block a user