Skip to content

Best Practices

Project Setup

Only specify widgets you use

Include a widget or icon in your spec files (inflater_spec.dart, icon_spec.dart) only if your UI uses it. Generated code holds a static reference to every specified constructor, so Flutter's tree shaking can't remove unused ones — every extra spec entry is dead weight in your app binary.

See Inflaters.

Check generated files into source control

Generated code depends on the exact Flutter version that produced it. Committing the .g.dart files means every teammate and CI pipeline builds from the same tested output instead of regenerating against whatever Flutter they happen to have — which is how "works on my machine" build breaks start.

Use the generated registry

Import and pass registerXWidgetComponents rather than calling the three per-component registrations directly. The registry imports the generated files for you, sets XWidget.config from your YAML config, and stays in sync automatically when you add or remove generated outputs.

import 'xwidget/generated/registry.g.dart';

await XWidget.initialize(register: registerXWidgetComponents);

See Registry.

Configure paths in one place

xwidget_config.yaml is the single source of truth for fragmentsPath and valuesPath. Don't hardcode paths in main() unless you have a runtime reason to override (tests, multi-tenant switching). Let the registry carry config into XWidget.config and let the default LocalResources pick them up.

Organize fragments into folders

Group each screen or feature's fragments in their own folder, and name a folder's entry fragment index.xml so it resolves by folder name ("settings" finds settings/index.xml). The recommended layout:

project
├─ lib
│  └─ xwidget          # specification files used for code generation
│     ├─ controllers   # custom controllers
│     └─ generated     # generated .g.dart files
└─ resources
    ├─ fragments       # XML fragments, one folder per screen/feature
    └─ values          # value resources i.e. strings.xml, bools.xml, etc.

Cloud Delivery

Keep baseline assets in the app even when using CloudResources

CloudResources falls back to the asset bundle when both the network and the local cache miss. On a first launch in airplane mode or after a cache wipe, your app will still be functional if you ship a baseline set of fragments and values. The resources/fragments and resources/values folders should exist in pubspec.yaml assets even for cloud-delivered apps. Symptoms of missing baselines are covered in Troubleshooting.

Wire channel and version to build-time flags

Hardcoding the channel in main() means staging and production builds come from different source trees — or worse, a dev accidentally ships a production build pinned to staging. Use --dart-define or build flavors:

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 explicit flags per environment:

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

Dependencies and Models

Give each page its own Dependencies instance

Pass a fresh Dependencies() when navigating to a page. A shared instance means one page's state bleeds into the next — a form value or callback left behind by page A silently shows up when page B reads the same key.

XWidget.navigateToFragment(
  "profile/settings",
  Dependencies(),
  context: context,
);

Let dependency scoping default to automatic

The tags that support dependenciesScope (<builder>, <callback>, <forEach>, <forLoop>, <fragment>) choose a sensible scope when the attribute is unset — for example, copy when loop variables or <var> declarations would otherwise leak into the parent. Set new, copy, or inherit explicitly only when you need to override that choice, and say why in a comment.

See Dependency Scoping.

Extend Model with explicit constructors

While it's convenient to use the Model class as-is, extending it with a constructor that declares each property as a parameter catches bad data at the call site instead of at render time:

// easy, but error prone — typos and missing keys surface later, in the UI
final profile = Model({
  "username": "mike.smith",
  "email": "[email protected]",
  "name": "Mike Smith"
});
// more verbose, but the compiler enforces the shape
final profile = Profile(
  username: "mike.smith",
  email: "[email protected]",
  name: "Mike Smith"
);

class Profile extends Model {
  String get username => getValue("username");
  String get email => getValue("email");
  String? get name => getValue("name");
  DateTime? get lastLogin => getValue("lastLogin");

  Profile({
    required String username,
    required String email,
    String? name,
    DateTime? lastLogin,
  }) : super({
    "username": username,
    "email": email,
    "name": name,
    "lastLogin": lastLogin,
  });
}

If you also need to load the model from a raw Map (an API response), register it with PropertyTransformers and add a named import constructor. See Loading Data.

XWidget.registerModel<Profile>(Profile.import, const [
  PropertyTransformer<String>("username"),
  PropertyTransformer<String>("email"),
  PropertyTransformer<String?>("name"),
  PropertyTransformer<DateTime?>("lastLogin"),
]);

class Profile extends Model {
  String get username => getValue("username");
  String get email => getValue("email");
  String? get name => getValue("name");
  DateTime? get lastLogin => getValue("lastLogin");

  Profile({
    required String username,
    required String email,
    String? name,
    DateTime? lastLogin,
  }) : super({
    "username": username,
    "email": email,
    "name": name,
    "lastLogin": lastLogin,
  });

  Profile.import(super.data, {super.translation, super.immutable});
}