Skip to content

Widget reactivity: support on-device state changes without a server push #165

@burczu

Description

@burczu

Problem

Voltra's current model resolves everything — colors, sizes, content — on the server into a static
JSON snapshot. The native side is a pure interpreter: it receives the snapshot and displays it.
SwiftUI and Glance both have reactive environment systems, but Voltra bypasses them entirely.

This means the following on-device state changes cannot be reflected in widgets without a new
server push:

  • System appearance (dark/light mode, high contrast, reduced motion)
  • Dynamic Type scale
  • iOS widgetRenderingMode (fullColor / accented / vibrant) — required for Liquid Glass and accented rendering
  • Android Material You dynamic color tokens
  • User-configurable widget parameters (iOS AppIntentConfiguration on iOS 17+; Android Glance config activities)
  • Time-based changes

The user-configurable parameter case is the hardest gap: when a user configures a widget in the
gallery, there is no server push — the widget process must render locally with the new parameter.
Today, Voltra has no mechanism for this on either platform.

Goal

Enable Voltra widgets to re-render in response to on-device state changes — primarily
user-configured parameters — without a server push and without an app update, while keeping the
RN-only developer experience (no Swift or Kotlin required from the developer).

Constraints

  • Server-driven layout must remain intact where possible — it is Voltra's core value proposition
  • Developer API stays in JSX + app.json; config plugin generates all platform-native boilerplate
  • iOS-specific: no new linked dependencies in the widget extension (extension memory budget is ~30 MB; JSC is preferred because it is a system framework with zero binary cost)
  • Android-specific: reuse Hermes if possible (already linked into every RN-on-Android app — zero added binary cost)

Context

The current server-driven JSON model has no mechanism for AppIntent / configurable-widget support
on either platform: when a user changes parameters, there is no server push, so the renderer has
nothing to apply the new values to. Each platform has its own architecture, so the approaches
explored on each are different — but the shared underlying claim is the same: some form of
on-device dynamic-value resolution is required, and we want to validate alternatives to natively-
interpreted opcode tuples (#130) and server-round-trip configurable widgets (#147)
.

Approaches explored — iOS

JSC-in-Extension (Track 2) — a thin JS resolver runs in the widget extension at render time.
Resolves {{ appIntent.X }} template expressions against current AppIntent parameters, then feeds
the resolved JSON into the existing Swift interpreter. Server retains layout control. Uses
JavaScriptCore (system framework — zero binary cost, no new linked dependency).

Build-Time Codegen (Track 3) — the Voltra component is compiled to self-contained SwiftUI at
build time by the config plugin. Full native reactivity, zero runtime overhead. Server can push
data props but not layout changes — an app update is required for structural changes.

A complementary iOS branch (Track 1, poc/widget-reactivity-track-1) addresses rendering
improvements that are independent of AppIntent reactivity: widgetAccentable prop support and
light-dark() CSS Color Level 4 syntax for adaptive text colors. It is a standalone concern that
can be merged separately and is not a prerequisite for either of the above approaches.

Approaches explored — Android

Hermes-in-Process (Track 4) — the same JS-resolver pattern as iOS Track 2, but adapted to
Android's architecture. Unlike iOS widget extensions, Glance widget updates run inside the main
app process via WorkManager — there is no sandboxed extension to slip a JS engine into. Instead,
Voltra instantiates a standalone Hermes runtime via a custom JNI/NDK wrapper, invoked from
VoltraGlanceWidget.provideGlance(). The shared JS bundle (same source as iOS Track 2) resolves
{{ appIntent.X }} placeholders against parameters stored in DataStore, populated by an in-app
UI (the PoC's stand-in for a future Glance configuration activity).

The architectural claim: one JS resolver bundle, two platforms, with strictly more expressiveness
than opcode tuples
. PoC scope is narrow — prove engine integration and the reactivity loop. Does
not (yet) replace the native opcode-tuple resolver from #130 or add Glance configuration
activities (analog of #147).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions