v4.11 — Kindling Widgets: Android Parity
The Android home-screen widget catches up to iOS's refined stone tower, earned Coach line, and tap-to-check-off — merged to
master, staging-live, still behind a 0%-rollout flag pending a human GA decision.
Summary
v4.7 refined the home-screen widget's streak block into a stone tower — a gold capstone when the streak was earned that day, an open slot when it wasn't, a warm gradient card, and one earned line of Coach copy — but that refinement shipped iOS-only. The Android widget kept rendering the pre-kindling v4.1/v4.2 treatment for another two months.
v4.11 ports that refinement to Android and closes the interaction gap at the same time: a tap on a due habit row now checks it off directly from the widget, without opening the app, matching the fast-follow iOS shipped in v4.7 Phase 5. Both land across three home-screen sizes — Small, Medium, and a new Large-equivalent breakpoint that Android didn't have room to reach before this milestone.
This was scoped and delivered as a refinement port, not a rewrite. The Android widget was already a substantial RemoteViews/AppWidgetProvider incumbent — adaptive layout budgets, fontScale bands, a working stones/flame streak metaphor, and a shared, platform-neutral snapshot that already carried coachLine and streakMetaphor fields Android simply wasn't rendering yet. Confirming that up front (Phase 1's decision gate) meant every later phase was additive to a proven architecture instead of a Jetpack Compose/Glance rewrite competing with it.
The milestone's other throughline: three separate times, a passing CI run concealed a real gap — a concurrency bug in the new check-off queue, a whole render path (kindling-on) that no test had ever actually inflated, and a metaphor combination (kindling × flame) that had never been rendered at any size. All three were caught by someone reading what the tests actually asserted, not by a test failing, and all three are now closed with real, device- or views.apply()-verified coverage.
All five phases shipped 2026-07-21, and the integration branch merged to master the same day (OBJ-1622, PR #1687) after Orion flagged that the branch — despite every phase being reviewed and shipped — had never actually reached master and therefore had zero staging or production exposure. That merge hit a real content conflict in docs/features/home-widgets.md (both master and the integration branch had independently edited the doc), reconciled by Dori and verified byte-for-byte by Roy before Vicki shipped it. Staging now serves this code. The kindling_widget_enabled flag remains at 0% rollout on both platforms — landing on master and rolling out to real users are two separate, deliberately decoupled steps.
Goal
Bring the Android home-screen widget to feature parity with the v4.7 Kindling refinement: the refined stone-tower streak, one earned Coach line, and a tappable check-off — delivered across Small / Medium / Large-equivalent home-screen sizes, in Objectuve's anti-dark-pattern brand voice. A verbatim pixel port of the iOS SwiftUI treatment is not required — an Android-idiomatic equivalent within RemoteViews' constraints is the bar.
Scope — What Shipped
- Confirmed RemoteViews over a Glance rewrite (ANDROID-DECIDE-1) — the incumbent's adaptive layouts, fontScale handling, and metaphor rendering were substantial enough that a rewrite would have discarded real, working infrastructure for no benefit this milestone needed.
- Earned Coach line, reusing the same
coachLinesnapshot field iOS already renders — no new GraphQL surface, no new flag. - Refined stone-tower streak block on Small and Medium — gold capstone, kindling open-slot, tinted habit rows — across all six
WidgetStatecases, flag-off byte-identical to the pre-kindling render. - A genuine third breakpoint, Large-equivalent, added to the existing single
<appwidget-provider>(not a second widget/second picker entry): tower header → Coach line → a 6-row-capped checklist with overflow → a pinned closing motivator. Reuses Phase 2's primitives with zero new colour tokens, drawables, or view types. - Tap-to-check-off, net-new on Android: a click
PendingIntentper due, unchecked habit row →onReceive→ a durable, natively-deduped pending-checkin queue inSharedPreferences→ drained into the existing offline sync queue on next app foreground → the sameCHECK_IN_HABIT_MUTATIONand server-side per-day idempotency every other check-in already uses. No new GraphQL surface, zero-lineAndroidManifest.xmldiff. - A ruling on kindling × FLAME: the refinement is correctly stone-tower-only by design — a flame user isn't degraded, since every metaphor-neutral part of the refinement (card, tinted rows, Coach line, CTA, the whole Large layout) still reaches them; only the streak glyph itself stays unchanged.
- Metaphor-axis render coverage closing the one real gap Phase 5's sweep found — 36 new goldens covering kindling-on × FLAME across all three sizes, six states, and both themes, every cell genuinely inflated, zero production source touched.
- The
masterintegration merge (OBJ-1622) — landing five already-reviewed phases onmasterafter 21 commits of drift, including hand-reconciling a genuine doc conflict rather than mechanically picking a side. - Docs:
docs/features/home-widgets.md's Android parity (v4.11) section, corrections to two statements the milestone made false, and this narrative page.
Phases
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 1 | Decision gate + Coach line wire-up | Shipped | 1 | RemoteViews confirmed over Glance; Coach line on Medium (Small deferred to Phase 2 as a design call, not an omission) |
| 2 | Visual refinement — Small + Medium | Shipped | 1 | Refined stone tower across all six states; absorbed a Roborazzi BLOCKER (kindling-on tree had never been test-inflated) and a merge-conflict fix against Phase 4 |
| 3 | Large-equivalent family | Shipped | 1 | Third breakpoint, not a second widget; raised maxResize* so Large is reachable by dragging; fixed a carried STALE-row tappability defect |
| 4 | Interactive check-off + write path | Shipped | 1 | Spike-gated on a real emulator; shipped with a Roy-caught concurrency fix (shared lock, proven load-bearing by reverting it and watching a 50-way interleaving test fail 5/5) |
| 5 | State-coverage sweep + docs | Shipped | 1 | Desi's kindling×FLAME ruling; 36 new goldens closing the metaphor-axis gap; docs correcting two now-false claims |
| — | Integration merge to master (OBJ-1622) | Shipped | — | Not a planned phase — a separate delivery step opened after Orion flagged zero staging/production exposure; hit and resolved a genuine doc conflict, confirmed staging-green post-merge |
Key Decisions
- RemoteViews, not Glance — the incumbent's adaptive layout budgets, fontScale bands, and stones/flame rendering were substantial and RemoteViews-native; a rewrite onto Jetpack Compose for widgets would have discarded all of it for no benefit this milestone needed.
- Refinement port, not from-scratch — source verification before Phase 1 showed Android already rendered a stones treatment from the shared snapshot, and the snapshot already carried
coachLine. This materially lowered the milestone's actual build scope from what the source issue assumed. - No new Android-specific flag —
kindling_widget_enabledwas already a single, platform-neutral flag written into the snapshot JSON; Android'sWidgetSnapshot.javasimply wasn't parsing the two new fields yet. Every phase reuses this one flag. - Coach line on Medium only at Phase 1, deliberately deferred on Small — Small has the tightest layout budget of any size, and whether it should carry a Coach line at all needed a design call, not an engineering guess. Resolved in Phase 2.
- Phase 3 depends on Phase 2, not just Phase 1 — corrected mid-milestone after the kickoff summary implied uniform parallelism; Phase 3's Large checklist explicitly reuses Phase 2's stone-tower primitives, so building it first would have meant inventing them twice.
- Kindling × FLAME is correct as shipped, ruled not assumed — the stone-tower refinement has no tower-shaped concept to port onto a flame, which already carries its own three-state signal (lit+halo / muted / plain) in its own vocabulary. Locked in by 36 goldens so a future change can't silently alter what a flame user sees.
- The tappable-row cap is 9, not arbitrary — the check-off
PendingIntent's request code is derived asappWidgetId * 10 + rowIndex, collision-free only whilerowIndex ≤ 9. Large's checklist — the first thing in this milestone that could plausibly exceed four rows — shipped capped at 6, with room to spare. - The
mastermerge was its own delivery step, not a docs-close formality — Orion insisted on naming this plainly rather than letting "milestone complete" quietly mean "merged to a branch nobody ships from." The merge surfaced a real doc conflict, requiring a genuine content-reconciliation decision rather than a mechanical rebase. - Landing on
masterand rolling out the flag are deliberately decoupled — the merge changes nothing user-visible on its own (fail-closed, 0% rollout on both platforms); GA is a separate, later, human call per repo policy.
Requirements Coverage
16 / 16 requirements satisfied (per the Milestone Audit).
| Category | Count | Status |
|---|---|---|
| ANDROID-DECIDE-* / ANDROID-COACH-* | 2 | All satisfied |
| ANDROID-REFINE-* (Small/Medium) | 3 | All satisfied |
| ANDROID-LARGE-* (Large breakpoint) | 2 | All satisfied |
| ANDROID-INTERACT-* (check-off write path) | 4 | All satisfied |
| DESIGN-FLAME-1 / DESIGN-TIDY-1 | 2 | All satisfied |
| ANDROID-COVERAGE-1 | 1 | Satisfied |
| DOCS-1 / DOCS-2 | 2 | All satisfied |
Full detail: v4.11-kindling-widgets-android-parity-MILESTONE-AUDIT.md.
Outcomes
Android and iOS home-screen widgets now render the same refined stone-tower streak block, the same earned Coach line, and support the same tap-to-check-off interaction — at parity across every size each platform supports, fail-closed throughout — in the code that ships to staging today. Verified live: /health on staging reports database/redis/sidekiq all ok on the commit superseding this merge.
Not yet true for any real user: kindling_widget_enabled is registered fail-closed at 0% rollout on both platforms, and no production tag has been cut for this work yet — it rides the next scheduled release train. Both are intentional, separate, later decisions, not gaps in this milestone.
Tech Debt
- Flag rollout —
kindling_widget_enabledstays 0% until a human promotes it; a separate call from this milestone's delivery. - No production tag yet — rides the next Wednesday 08:00 CT release train.
- (Phase 5, carried forward) Fresh-install
FIRST_RUNreal-device placement has never been performed in any environment this milestone. Tracked as OBJ-1624 (backlog, may need real device/emulator-with-launcher access this milestone never had). - (Phase 4, carried forward) The same unguarded read-then-clear race Phase 4 fixed on Android also exists on iOS, at
WidgetBridgePlugin.swift:61-74— pre-existing, live in production. Tracked as OBJ-1623. - (Phase 5, carried forward) Flame-glyph contrast floor on the kindling card — pre-existing (v4.2), not a v4.11 regression. Tracked as OBJ-1625.
Related Artifacts
- Roadmap: v4.11-kindling-widgets-android-parity-ROADMAP.md
- Milestone Audit: v4.11-kindling-widgets-android-parity-MILESTONE-AUDIT.md
- Feature docs: docs/features/home-widgets.md § Android parity (v4.11)
- Git tag: none — rides a future release once the flag rolls out
- Phase PRs: #1658 (Coach line) · #1673 (Small + Medium refinement) · #1684 (Large breakpoint) · #1671 (interactive check-off) · #1685 (coverage sweep + docs)
- Integration-merge PR: #1687
- Source issue: OBJ-1587 · Milestone anchor: OBJ-1588 · Integration-merge issue: OBJ-1622
Related Commits
de09f74b6— Phase 1: decision gate + Coach line (#1658)f70ce20a3— Phase 2: Small + Medium visual refinement (#1673)4a3a1834f— Phase 3: Large-equivalent breakpoint (#1684)c08cbff0b— Phase 4: interactive check-off + write path (#1671)12af63a3f— Phase 5: state-coverage sweep + docs (#1685)65e3b5a29— Integration merge tomaster(#1687)
Last updated: 2026-07-21