Skip to content

Tips and Tricks

Regenerate inflaters after upgrading Flutter

Generated inflaters call the widget constructors of the Flutter version that produced them. After a Flutter upgrade, regenerate so changed signatures, new parameters, and removed deprecations are reflected — otherwise the mismatch surfaces as runtime errors in inflated fragments:

xc generate

Or without the xc command:

dart run xwidget_builder:generate

Regenerate after config changes

Changing fragmentsPath or valuesPath in xwidget_config.yaml requires regeneration — the paths are baked into registry.g.dart:

xc generate --only inflaters

The registry regenerates with every run, even when --only specifies another component. Your main() doesn't need to change.

Switch between local and cloud resources at runtime

For debugging cloud-delivered UI against a local override, or for conditional cloud usage, pass different Resources instances based on configuration:

const useCloud = bool.fromEnvironment('USE_CLOUD', defaultValue: false);

await XWidget.initialize(
  register: registerXWidgetComponents,
  resources: useCloud
      ? CloudResources(
          projectKey: '...',
          storageKey: '...',
          channel: 'production',
          version: '1.0.0',
        )
      : LocalResources(),
);

Build with --dart-define=USE_CLOUD=true to opt into cloud.

Use --schema-docs html for IntelliJ-based IDEs

If you're developing with Android Studio or IntelliJ IDEA, generate the schema with HTML doc formatting for readable tooltips:

xwidget_config.yaml
schema:
  documentationFormat: html

Or override for a single run:

xc generate --schema-docs html

See Code Generation Overview for all generator flags.

Use controllers to create reusable components

A controller bound to a fragment travels with it — inflate the same fragment from three screens and each gets the fragment's behavior without duplicating any Dart. Keep the logic in the controller and the fragment purely presentational, and the pair becomes a reusable component.

See Controllers.

Declare model collections as <String, dynamic>

When something listens to a value — <ValueListener> in a fragment, or listenForChanges in Dart — XWidget wraps that value in a ModelValueNotifier and stores the wrapper back into the enclosing collection. If Dart inferred a narrow type for that collection (say Map<String, Map<String, String>>), the wrapper doesn't fit and the write throws a runtime type error:

// avoid — Dart infers Map<String, Map<String, String>>
final model = Model({
  "users": {
    "user1": { "email": "@", "phone": "0" },
    "user2": { "email": "@", "phone": "0" }
  }
});

model.listenForChanges("users.user1", null, null);   // throws — wrapper doesn't fit

Declare the collections loosely so the notifier wrapper always fits:

// ok — explicitly <String, dynamic>
final model = Model({
  "users": <String, dynamic>{
    "user1": <String, dynamic>{ "email": "@", "phone": "0" },
    "user2": <String, dynamic>{ "email": "@", "phone": "0" }
  }
});

model.listenForChanges("users.user1", null, null);   // works

In practice you rarely call listenForChanges directly — <ValueListener> does it for you — but the typing rule applies either way. Data loaded through registered PropertyTransformers doesn't have this problem; it's a concern for maps you build by hand.