Best Practices
Do use fragment folders
Using fragment folders helps keep your project organized by grouping related UI components together. Fragments should be stored in appropriately named folders to improve maintainability and scalability. Ensure that each fragment is self-contained and follows a consistent naming convention.
Don't specify unused widgets
Only include references to widgets and icons in your specification files
(inflater_spec.dart, icon_spec.dart) if you intend to use them in your application.
This is because all referenced elements will be bundled with your app, regardless of
whether they are used, and will not be removed by Flutter’s tree shaking optimization.
Do check-in generated files into source control
Since the generated code is used dynamically and is highly dependent on the installed version of Flutter, we believe it is best to check in XWidget’s generated files to ensure build stability. This prevents inconsistencies that may arise from differences in Flutter versions or code generation tools across development environments. By committing these files, we ensure that all team members and CI/CD pipelines work with a consistent, tested version of the generated code, reducing the risk of unexpected issues caused by regeneration discrepancies.
Recommended folder structure
A well-structured project enhances maintainability and code clarity. Below is a recommended folder structure for organizing XWidget-based projects:
project
├─ lib
│ └─ xwidget # holds all specification files used for code generation
│ ├─ controllers # holds all custom controllers
│ └─ generated # holds all generated .g.dart files
└─ resources
├─ fragments # holds all fragments
└─ values # holds all resource values i.e strings.xml, bools.xml, etc.
Following this structure ensures a clean separation of concerns, making the project easier to navigate and manage.
Instantiate a new Dependencies object for each page
Each page should have its own Dependencies object to ensure proper dependency isolation. This prevents unintended side effects from shared dependencies and maintains modularity.
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return Scaffold(
body: XWidget.inflateFragment("profile/settings", Dependencies())
);
}));
Prefer automatic scoping of Dependencies within Fragments
Automatic scoping of dependencies ensures that each component receives the correct dependencies
based on its usage. This can reduce unexpected side effects when dependencies are
scoped incorrectly. Use the dependenciesScope attribute on tags that support it i.e.
new, copy, inherit - leave empty for
auto scoping.
Do extend Model and create constructors with explicit properties
While it's convenient to use the Model class as-is, extending the Model class and creating a
constructor with all your model's properties defined as parameters is a more robust approach.
This method ensures that all data is validated before it enters your model, helping prevent
errors down the road.
// easy, but error prone
final profile = Model({
"username": "mike.smith",
"email": "mike.smith@example.com",
"name": "Mike Smith"
});
// more verbose, but more stable and fewer potential errors
final profile = Profile(
username: "mike.smith",
email: "mike.smith@example.com",
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 need to load data from a Map structure, then override propertyTransformers getter
and add an import factory constructor. See Model -> Loading Data for more
information.
Note: In the near future, XWidget will have the ability to generate most of the required code for you.
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});
}