Server-driven widgets
Server-driven widgets allow your Android Home Screen widgets to periodically fetch fresh content from a remote server—without the user opening the app. This is powered by WorkManager, which handles scheduling, retries, and network constraints automatically.
Before you start, make sure the widget is registered in the Voltra plugin config and plan to rebuild the native app after adding or changing server-driven widget settings.
How it works
- You configure a
serverUpdateURL in your Android widget's plugin config - WorkManager runs a periodic background task at the configured interval
- Your server renders Voltra JSX components into a JSON payload
- The worker parses the payload and pushes a
RemoteViewsupdate to the widget
Your app doesn't need to be running. WorkManager handles everything in the background.
Plugin configuration
Add the serverUpdate option to your Android widget in app.json or app.config.js:
serverUpdate options:
url: The Voltra SSR endpoint that returns widget JSON. Voltra appendswidgetId,platform, andthemequery parameters automatically (e.g.?widgetId=dynamic_weather&platform=android&theme=dark).intervalMinutes: How often the widget fetches updates. Defaults to15. The minimum effective interval is 15 minutes (WorkManager requirement).refresh: Whether to show a native refresh button in the top-right corner of the widget. When tapped, triggers an immediate server fetch. Defaults tofalse.
After updating plugin configuration, run npx expo prebuild if you're using Continuous Native Generation, then rebuild the app so the generated native widget code picks up the new server update settings.
On the Android emulator, use 10.0.2.2 instead of localhost to reach the host machine. Real devices need the host's LAN IP address.
Building the server
Voltra provides widget server handlers for the common runtime styles. Use createWidgetUpdateHandler() for Fetch-compatible runtimes, createWidgetUpdateNodeHandler() for node:http, and createWidgetUpdateExpressHandler() for Express-style handlers. All three share the same request parsing, platform validation, token validation, and response serialization.
The handler responds to GET requests with these query parameters:
The User-Agent header is set to VoltraWidget/<version> (Android/<version>).
Authentication
Widgets on Android are part of the main app binary, so the WorkManager background worker can access credential storage directly. Voltra encrypts credentials at rest using Google Tink (AES-256-GCM with Android Keystore-backed key management) and persists them in Jetpack DataStore.
Setting credentials
Call setWidgetServerCredentials after the user logs in:
The token is required and is sent as Authorization: Bearer <token> on every server request. Any additional headers are also included. If your widget endpoint does not require authentication, skip setWidgetServerCredentials() entirely.
Clearing credentials
Call clearWidgetServerCredentials when the user logs out:
All widgets are automatically reloaded after credentials are cleared, so they revert to their default/unauthenticated state immediately.
Refresh button
Server-driven widgets can display a native refresh button that lets users trigger an immediate update on demand. Enable it in your widget config:
When enabled, a small circular button (↻) appears in the top-right corner of the widget. Tapping it performs an inline HTTP fetch, generates new RemoteViews, and pushes the update directly to the widget—all without waiting for the next WorkManager cycle.
The refresh callback bypasses Glance's update() method (which doesn't reliably trigger provideGlance()) and instead uses GlanceRemoteViews.compose() to generate RemoteViews that are pushed directly via AppWidgetManager.updateAppWidget().
Resize handling
Your server should return all size variants in every response. When the user resizes a widget on the home screen, Voltra re-renders from cached data—no network request is made. The RemoteViews(sizeMapping) mechanism automatically picks the closest matching variant.
Triggering manual refreshes
You can force-refresh server-driven widgets outside of the regular interval:
For server-driven widgets, this enqueues an immediate one-time WorkManager request to fetch fresh content. For local-only widgets, it re-renders from cached data.
Initial state
Server-driven widgets still need content to display before the first server fetch completes. Use initialStatePath to provide a pre-rendered default:
See Widget pre-rendering for details on creating initial state files.
Provide a meaningful initial state (e.g. "Loading..." or placeholder content) rather than leaving it empty. The user sees this until the first server fetch succeeds.
Cross-platform server
A single server can handle both iOS and Android requests using createWidgetUpdateHandler:
The handler uses the required platform query parameter to route requests to the correct render function.
If you're serving the endpoint from Node or Express, use createWidgetUpdateNodeHandler() or createWidgetUpdateExpressHandler() instead.
Architecture overview
WorkManager handles scheduling, network constraints, and retries. The background worker reads credentials from encrypted storage, makes the HTTP request, parses the response, generates RemoteViews, and pushes the update via AppWidgetManager.
Error handling and retries
WorkManager automatically handles failures with exponential backoff. After 5 consecutive failed attempts, the worker gives up to avoid infinite retry loops. The next periodic run will start fresh.
- Network unavailable: The request is deferred until connectivity is restored (via
NetworkType.CONNECTEDconstraint). - Server errors (non-2xx): The worker retries with exponential backoff, up to 3 attempts.
- Empty response: The worker retries with exponential backoff, up to 3 attempts.
- Parse errors: If the JSON is stored but parsing fails, the data is still saved so Glance can attempt to use it later. This counts as a success since the data is persisted.
