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:
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.
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});
}