Skip to content

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 coachLine snapshot 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 WidgetState cases, 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 PendingIntent per due, unchecked habit row → onReceive → a durable, natively-deduped pending-checkin queue in SharedPreferences → drained into the existing offline sync queue on next app foreground → the same CHECK_IN_HABIT_MUTATION and server-side per-day idempotency every other check-in already uses. No new GraphQL surface, zero-line AndroidManifest.xml diff.
  • 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 master integration merge (OBJ-1622) — landing five already-reviewed phases on master after 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

PhaseNameStatusPlansHighlights
1Decision gate + Coach line wire-upShipped1RemoteViews confirmed over Glance; Coach line on Medium (Small deferred to Phase 2 as a design call, not an omission)
2Visual refinement — Small + MediumShipped1Refined 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
3Large-equivalent familyShipped1Third breakpoint, not a second widget; raised maxResize* so Large is reachable by dragging; fixed a carried STALE-row tappability defect
4Interactive check-off + write pathShipped1Spike-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)
5State-coverage sweep + docsShipped1Desi's kindling×FLAME ruling; 36 new goldens closing the metaphor-axis gap; docs correcting two now-false claims
Integration merge to master (OBJ-1622)ShippedNot 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 flagkindling_widget_enabled was already a single, platform-neutral flag written into the snapshot JSON; Android's WidgetSnapshot.java simply 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 as appWidgetId * 10 + rowIndex, collision-free only while rowIndex ≤ 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 master merge 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 master and 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).

CategoryCountStatus
ANDROID-DECIDE-* / ANDROID-COACH-*2All satisfied
ANDROID-REFINE-* (Small/Medium)3All satisfied
ANDROID-LARGE-* (Large breakpoint)2All satisfied
ANDROID-INTERACT-* (check-off write path)4All satisfied
DESIGN-FLAME-1 / DESIGN-TIDY-12All satisfied
ANDROID-COVERAGE-11Satisfied
DOCS-1 / DOCS-22All 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 rolloutkindling_widget_enabled stays 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_RUN real-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.
  • 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 to master (#1687)

Last updated: 2026-07-21

Loading…