Skip to content

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:

  1. 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 GET with the cached ETag when available.
  2. If the server returns 304 Not Modified, loads the cached bundle.
  3. If the pointer names the revision already in the cache, reuses the cached bundle without downloading.
  4. Otherwise downloads the immutable bundle the pointer references, verifies its SHA-256 against the pointer, and caches it.
  5. 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 from projectKey so 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 to XWidget.config.fragmentsPath.
  • valuesPath — override the values path. Defaults to XWidget.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:

xc cloud project keys -p "my-app"

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:

flutter build apk --dart-define=CHANNEL=production --dart-define=APP_VERSION=1.2.0

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:

  1. Previously cached bundle — loaded from disk. This is the common case for offline launches or transient network issues.
  2. Local assets — if no cached bundle exists (first launch, cache cleared), the resources in your app's asset bundle at fragmentsPath and valuesPath are 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.
  • projectKey and storageKey are 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.