Skip to content

feat(android): configure Dynamic Widgets per placed instance - #218

Open
V3RON wants to merge 9 commits into
mainfrom
feature/android-widget-instance-config
Open

feat(android): configure Dynamic Widgets per placed instance#218
V3RON wants to merge 9 commits into
mainfrom
feature/android-widget-instance-config

Conversation

@V3RON

@V3RON V3RON commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What is this?

This PR lets each placed Android Dynamic Widget hold its own configuration, so two placements of the same widget can show different data — different accounts, cities, lists, or filters. Until now Android configuration was scoped to the widget type, which meant every placement of a widget necessarily displayed the same values.

How does it work?

Configuration resolves in three layers, merged when the widget renders: code defaults declared as appIntent.parameters[].default, then values set for the widget type, then values set for one specific placed instance. The most specific layer wins.

Instance values are addressed by the Android appWidgetId and managed from inside your app:

  • setWidgetInstanceConfiguration(appWidgetId, key, value) — or pass an object to write several keys as a single update, rather than one widget re-render per key.
  • getWidgetInstanceConfiguration(appWidgetId) — reads back the fully merged configuration the widget actually sees, so a settings screen can open showing current values.
  • getWidgetConfiguration(widgetId) — the same for the widget-type layer.
  • clearWidgetInstanceConfiguration(appWidgetId) — hands an instance back to the widget-type and default values.

Writes are rejected for widgets belonging to another app and for server-rendered widgets, which never read env.configuration. When a user removes a widget from the home screen its instance values are deleted; because Android recycles appWidgetIds, this keeps a newly placed widget from inheriting a deleted one's configuration.

WidgetInfo from getActiveWidgets gains appWidgetId and widgetType. The existing widgetId and name fields still work, but are misleadingly named — widgetId is the instance id, not the widget's ID — so the new names are preferred.

Why is this useful?

Multiple instances of the same widget can finally be meaningfully different, which is the common reason to place a widget twice. Existing type-level configuration keeps working unchanged, and values stored under the older key format are migrated automatically.

The Android-native placement-time configuration flow — a configuration activity launched by the launcher when a widget is placed — was explored here and is deliberately left for a follow-up. Wiring android:configure changes placement semantics for every widget that declares appIntent parameters: the launcher discards a widget whose configuration activity does not complete. That flow is better delivered as a generated native form driven by the declared parameter schema, together with widgetFeatures="reconfigurable|configuration_optional" so placement is never blocked and users can reconfigure from the launcher.

V3RON added 6 commits June 24, 2026 11:54
Add per-instance Android widget configuration support, generated configuration trampolines, and example app wiring for configurable widgets.
Adds Android Dynamic Widget configuration schema validation for deep-link setup.
Rejects malformed configuration values before deepLink validation.
Adds bare React Native CLI config parity for Dynamic Widget configuration deep links.
…t-instance-config

# Conflicts:
#	packages/cli/src/config/normalize.ts
#	packages/cli/src/config/types.ts
#	packages/cli/src/index.ts
#	packages/cli/test/cli.test.js
Resolve project-relative Dynamic Widget client source paths during Android placeholder prerendering and defer file-generation detection until Expo provides projectRoot.
@Angelk90

Angelk90 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@V3RON :

  1. What's the point of having setWidgetConfiguration? If you have setWidgetInstanceConfiguration.
  2. If I have two weather widgets installed on my home screen, one configured for London and one for New York, and the data displayed is about an hour old because it hasn't been updated yet, I want to be able to initiate an update directly from the app.

When the user opens the app and requests a refresh for all widgets (for example, by clicking a button), both widgets should receive the new weather data and automatically update, displaying the latest information for their respective cities, just like system weather apps like Samsung Weather.

Is this possible with the current code?

@Angelk90

Angelk90 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@V3RON: If I need to set multiple values, do I have to call setWidgetInstanceConfiguration multiple times?

 await setWidgetInstanceConfiguration(parsedAppWidgetId, 'theme', 'dark')
 await setWidgetInstanceConfiguration(parsedAppWidgetId, 'language', 'ko')
 await setWidgetInstanceConfiguration(parsedAppWidgetId, 'userId', '123')
 await completeWidgetConfiguration(parsedAppWidgetId)

I would prefer something like this:

setWidgetInstanceConfigurations(
  appWidgetId: number,
  values: Record<string, string>,
): Promise<void>;
setWidgetInstanceConfigurations(parsedAppWidgetId, {
  theme: "dark",
  language: "ko",
  userId: "123",
});

Narrow this branch to the in-app instance configuration API and postpone the
placement-time configuration flow.

Configuration now resolves in three layers — code defaults, widget-type values,
then per-instance values. Instance values are written, read and cleared from
inside the app via setWidgetInstanceConfiguration,
getWidgetInstanceConfiguration, getWidgetConfiguration and
clearWidgetInstanceConfiguration, and are removed when a widget is deleted so a
recycled appWidgetId cannot inherit stale values.

Drop the generated configuration-activity trampolines, android:configure wiring,
configuration.deepLink option and deep-link scheme derivation. Adding
android:configure changes placement semantics for every widget that declares
appIntent parameters, and the flow is better served by a schema-driven native
form using widgetFeatures="reconfigurable|configuration_optional".
@V3RON V3RON changed the title feat(android): add widget instance configuration feat(android): configure Dynamic Widgets per placed instance Aug 12, 2026
V3RON added 2 commits August 12, 2026 10:37
Add a second configuration key (subtitle) to the Android demo widget so a
batched write can be observed updating both rows in one re-render, a
widget-type configuration control so the shared layer and its shadowing
behaviour are reachable from the UI, and a button that deliberately targets a
server-rendered widget to exercise the Dynamic-Widget-only rejection.

The widget renders subtitle with an "(unset)" fallback so it also works before
a rebuild picks up the new appIntent default.
The pin screen had grown to 525 lines covering two unrelated jobs. Move
the instance and widget-type configuration UI to a dedicated screen,
reachable from the Widgets tab, and return the pin screen to pinning.

Report results in an inline status line rather than an Alert, so the
home screen tile can be watched without dismissing a dialog first, and
show the merged env.configuration read-back after every write to make
layer shadowing directly visible. Inputs are generated from the demo
widget's declared keys instead of copy-pasted per key.

Also fix the "Reset to Defaults" preview button, which passed the
gesture event into resetPreviewDimensions' widget parameter and wrote
"undefined" into both dimension fields.
@V3RON

V3RON commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@Angelk90 Batching is in — it landed after your comment, so you would not have seen it. Rather than a separate plural function, setWidgetInstanceConfiguration is overloaded:

// One key
await setWidgetInstanceConfiguration(appWidgetId, "theme", "dark")

// Several at once — your case
await setWidgetInstanceConfiguration(appWidgetId, {
  theme: "dark",
  language: "ko",
  userId: "123",
})

The object form is one bridge call, one DataStore transaction, and one widget re-render, instead of one of each per key. So it is what you asked for, just under the singular name.

On the naming specifically: I went with an overload rather than adding setWidgetInstanceConfigurations because two exported names differing only by a trailing s sit next to each other in autocomplete, and picking the wrong one still compiles. One name with two typed signatures avoids that, and there is only one thing to learn.

As for setWidgetConfiguration — it stays, and it is not redundant with the instance API:

  • It is the only layer that can apply to placements that do not exist yet. There is no appWidgetId before a widget is placed, so "every future placement starts with these values" is not expressible per-instance.
  • It is the right scope for genuinely app-wide settings — units, theme, locale — where a per-placement value would be the wrong model.
  • It is existing public API; removing it would be a breaking change for anyone already using it.

The two layers compose: instance values shadow widget-type values, and clearWidgetInstanceConfiguration hands an instance back to the shared value.

Your second question, about refreshing both widgets from inside the app, deserves its own answer — replying to that separately.

@V3RON

V3RON commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

One note on scope, following up on my earlier reply.

Configuration in this PR applies to Dynamic (client-rendered) widgets only — setWidgetInstanceConfiguration and friends reject server-rendered widgets with an IllegalStateException. That is deliberate for now: server-rendered widgets never read env.configuration, so silently accepting writes that could never affect the render would be worse than failing loudly.

It is a statement about the current render path, not a permanent boundary. Making server-driven widgets configurable per instance is planned as a separate PR, where the merged configuration would be passed to the SSR endpoint so each placement can be rendered with its own values.

That work is larger than it first looks, which is why it is not bundled here. Today both the server URL (server_url_<widgetId>) and the cached payload (KEY_JSON_PREFIX + widgetId) are keyed by widget type, so per-instance responses need an instance-keyed storage layer of their own — plus scheduling, cleanup on delete, and recycled-appWidgetId safety, mirroring what this PR does for configuration. Changing a configuration value would also need to trigger a refetch rather than a re-render from cache.

Nothing in this PR forecloses that; the instance-id plumbing added here is the groundwork for it.

@Angelk90

Angelk90 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@V3RON : One thing I don't understand, you say: PR applies to dynamic (client-rendered) widgets only

But with this PR or the current repository, if I have a widget and want it to update itself every 30 minutes, is it possible to do so?
Example: I created a weather widget, I have the widget component created, and it needs to get information from a nodeJs/php API that returns the JSON info every 30 minutes to update the data.
Is it possible to do this at this point?

I'm talking without using server-rendered.

If I haven't explained myself well, I'll try to explain it again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants