CloudResources
CloudResources loads fragments and values from the XWidget Cloud content server.
It enables over-the-air UI updates — push changes to any channel, and running apps
pick them up on their next launch.
When to Use
- Synchronized UI across platforms — ship a change once, iOS and Android pick it up at the same moment. No waiting on app store review while users on the other platform already have the update.
- Real-time user engagement — time-sensitive offers, live events, countdowns, and promotions that require UI updates on their own schedule, not yours.
- Consistent experience across your install base — users who rarely update the app still receive the latest UI, giving you control over what they actually see.
- A/B testing and gradual rollouts — deploy UI variants to different channels or versions, split traffic, and compare results without shipping a new binary.
For apps that ship their entire UI inside the app binary, see
LocalResources. If you want XWidget Cloud analytics but keep UI
bundled, see LocalResources.withAnalytics.
Resource Loading Modes
XWidget.initialize() supports three resource-loading modes, chosen by which
Resources implementation you pass:
| Mode | Resources Class | Resources From | Analytics |
|---|---|---|---|
| Local only | LocalResources() (default) |
Asset bundle | No |
| Local + analytics | LocalResources.withAnalytics(...) |
Asset bundle | Yes |
| Cloud | CloudResources(...) |
Content server | Yes |
See LocalResources for the first two modes. The rest of this page covers the cloud mode.
Basic Usage
import 'package:xwidget/xwidget.dart';
import 'xwidget/generated/registry.g.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await XWidget.initialize(
register: registerXWidgetComponents,
resources: CloudResources(
projectKey: '<your-project-key>',
storageKey: '<your-storage-key>',
channel: 'production',
version: '1.0.0',
),
);
runApp(MyApp());
}
On startup, CloudResources:
- Fetches the channel's pointer record for the app's version — a small JSON file
naming the revision the channel currently serves — using a conditional
GETwith the cached ETag when available. - If the server returns
304 Not Modified, loads the cached bundle. - If the pointer names the revision already in the cache, reuses the cached bundle without downloading.
- Otherwise downloads the immutable bundle the pointer references, verifies its SHA-256 against the pointer, and caches it.
- If any step fails, falls back to the previously cached bundle; if no cache exists, falls back further to assets bundled in the app binary at the same paths.
Parameters
CloudResources({
required String projectKey,
required String storageKey,
required String channel,
required String version,
String? fragmentsPath,
String? valuesPath,
Duration downloadTimeout = const Duration(seconds: 15),
});
projectKey(required) — your XWidget Cloud project key, used to authenticate analytics.storageKey(required) — your XWidget Cloud storage key, used to locate the bundle on the content server. Separate fromprojectKeyso you can rotate one without invalidating the other.channel(required) — deployment channel to pull from ('production','staging', etc.).version(required) — the app version. Determines which bundle version the server serves.fragmentsPath— override the fragments path. Defaults toXWidget.config.fragmentsPath.valuesPath— override the values path. Defaults toXWidget.config.valuesPath.downloadTimeout— how long to wait for the initial bundle download before falling back to cache. Defaults to 15 seconds.
Retrieving Keys
Your project's projectKey and storageKey are available via the CLI:
See Projects for details on key retrieval and rotation.
Important
Do not hard-code keys directly in your source. Use a remote configuration service (such as Firebase Remote Config) to deliver keys and channel settings to your app. This allows you to rotate keys, switch channels, and respond to incidents without shipping an app update.
Channels and Versions
XWidget Cloud organizes bundles by channel and version. The channel identifies the environment or audience, and the version identifies a specific build of your app that the bundle targets.
Typical usage:
// Staging build
CloudResources(
projectKey: '...',
storageKey: '...',
channel: 'staging',
version: '1.2.0',
);
// Production build
CloudResources(
projectKey: '...',
storageKey: '...',
channel: 'production',
version: '1.2.0',
);
Which revision of the version the app receives is decided entirely by the channel's pointer — apps never reference revisions. Publishing a new revision (or rolling back to an old one) takes effect on the next launch, with no client changes. The revision the app is running is reported in analytics so you can watch a publish roll out across your install base.
You typically wire the channel and version to build-time configuration (or to
values fetched from Firebase Remote Config) so the same source produces different
binaries for different environments. Using --dart-define:
const channel = String.fromEnvironment('CHANNEL', defaultValue: 'staging');
const version = String.fromEnvironment('APP_VERSION', defaultValue: '0.0.0');
await XWidget.initialize(
register: registerXWidgetComponents,
resources: CloudResources(
projectKey: '...',
storageKey: '...',
channel: channel,
version: version,
),
);
Then build with:
See Deployments for how bundles are deployed to channels.
Caching Behavior
Bundle caching is automatic and persistent across app launches. The cache stores the bundle bytes plus metadata recording which channel, version, and revision they belong to, their SHA-256, and the pointer's ETag. The cache is only trusted when its channel and version match the current configuration — switching channels invalidates it automatically. On each launch:
- Pointer unchanged — server returns
304, app loads from local cache. Fast startup, no bundle transferred. - Pointer changed, revision already cached — the pointer was rewritten but still names the cached revision; the app reuses the cache without downloading.
- Pointer names a new revision — app downloads the bundle, verifies its SHA-256 against the pointer, updates the cache, loads the fresh bundle.
- Cache miss — app downloads, verifies, populates the cache, loads.
Cache hits and downloads are tracked as analytics events. Network errors are tracked separately. See Analytics.
Fallback Behavior
When the content server can't be reached or returns an error, CloudResources walks
a fallback chain:
- Previously cached bundle — loaded from disk. This is the common case for offline launches or transient network issues.
- Local assets — if no cached bundle exists (first launch, cache cleared), the
resources in your app's asset bundle at
fragmentsPathandvaluesPathare used.
You should always ship a baseline set of fragments and values in your app's assets, so first launches and post-cache-clear launches have something to render.
Full Example
import 'package:flutter/material.dart';
import 'package:xwidget/xwidget.dart';
import 'xwidget/generated/registry.g.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Cloud settings should be loaded from Firebase Remote Config
// or a similar service — not hard-coded.
await XWidget.initialize(
register: registerXWidgetComponents,
resources: CloudResources(
projectKey: '<your-project-key>',
storageKey: '<your-storage-key>',
channel: 'production',
version: '1.0.0',
),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return XWidget.inflateFragment("my_app", Dependencies());
}
}
Security Notes
- The bundle's SHA-256 is verified against the hash in the channel's pointer record on every fresh download. Tampered bundles are rejected before they're loaded.
- Bundles are immutable — once deployed, a revision's bytes never change. Only the small pointer record moves.
projectKeyandstorageKeyare separate to support rotation. See Key Rotation.- Bundles are served over HTTPS.
Custom Resource Providers
If you need to self-host bundles or implement a custom delivery mechanism (behind a
VPN, from your own content server, with custom authentication), don't modify
CloudResources. Extend Resources directly instead. CloudResources is the XWidget
Cloud client — it's designed for XWidget Cloud's content server, pointer conventions,
and analytics endpoints. For a different delivery path, write a new subclass. You can use
CloudResources as a reference for the download / verify / cache / fallback pattern.
class MyHostedResources extends Resources {
@override
Future<void> load() async {
// Your download, verification, and bundle-registration logic here.
// Populate FragmentResourceBundle and ValueResourceBundle, then:
replaceResourceBundles([fragments, values]);
}
}
See the CloudResources source for the pattern.
Testing
For unit tests, use LocalResources with a TestAssetBundle. Mocking the network
layer of CloudResources for tests is rarely worth the effort — test against
LocalResources and trust the cloud provider's network path to integration tests.