Best Practices
Guidelines for designing effective property collections.
Property Key Naming
Use consistent, descriptive keys in camelCase:
// Good
b.string('courseTitle', 'Course Title');
b.integer('maxCapacity', 'Max Capacity');
b.boolean('enableNotifications', 'Enable Notifications');
// Avoid
b.string('title1', 'Course Title'); // numbered keys are ambiguous
b.integer('max_cap', 'Max Capacity'); // inconsistent casing
b.boolean('notif', 'Enable Notifications'); // abbreviationsGroup Organization
Organize properties into logical groups. Place the most commonly edited properties in the first group, and less frequently changed settings in later groups:
b.group('basic', 'Basic Information'); // most common
b.group('enrollment', 'Enrollment'); // important but secondary
b.group('advanced', 'Advanced',
initiallyCollapsed: true); // rarely changedUse initiallyCollapsed: true for advanced or rarely-used groups.
Validation Placement
Prefer adding validators at property declaration time rather than validating at save time:
// Good: validators on the property
b.string('email', 'Email',
required: true,
validators: [
PropertyValidators.email(),
PropertyValidators.maxLength(255),
],
);
// Avoid: validating only at save time
void save() {
final email = collection.getValue<String>('email');
if (!isValidEmail(email)) { /* ... */ } // error-prone
}The built-in editors show validation errors inline as the user types, providing immediate feedback.
Condition Dependencies
Declare dependent properties after their dependencies. This ensures the dependency exists when the collection sets up reactions:
// Good: dependency first, dependent second
b.boolean('advanced', 'Show Advanced');
b.integer('threadCount', 'Threads',
visibleCondition: PropertyCollectionBuilder.whenTrue('advanced'));
// This will throw: 'threadCount' depends on 'advanced' which hasn't been added yet
b.integer('threadCount', 'Threads',
visibleCondition: PropertyCollectionBuilder.whenTrue('advanced'));
b.boolean('advanced', 'Show Advanced');Prefer Builder Over Manual Construction
The PropertyCollectionBuilder handles group assignment, ordering, and derivation wiring automatically:
// Good: builder handles everything
final b = PropertyCollectionBuilder();
b.group('basic', 'Basic');
b.string('title', 'Title', required: true);
b.integer('duration', 'Duration', defaultValue: 60);
final collection = b.buildCollection();
// Verbose: manual construction requires more bookkeeping
final collection = PropertyCollection(
groups: [PropertyGroup.collapsible(value: 'basic', title: 'Basic')],
properties: [
StringProperty(key: 'title', label: 'Title', group: 'basic', required: true),
IntProperty(key: 'duration', label: 'Duration', group: 'basic', defaultValue: 60),
],
);Dispose Collections
Always dispose collections when they are no longer needed to cancel stream subscriptions:
class _MyWidgetState extends State<MyWidget> {
late final PropertyCollection _collection;
@override
void initState() {
super.initState();
_collection = buildSettings();
}
@override
void dispose() {
_collection.dispose();
super.dispose();
}
}Alternatively, set disposeCollection: true on PropertyCollectionEditor to let the editor handle disposal.
Use Custom Keys Sparingly
Custom keys are powerful but add complexity. Use them for:
- Specialized editors (e.g.,
slider:int,colorPicker:string) - Read-only variants (already handled by
ReadOnlyProperty) - Domain-specific converters
For most properties, the default type-based lookup is sufficient.
Keep Conditions Declarative
Prefer built-in condition types over CustomCondition when possible:
// Good: declarative, self-documenting
visibleCondition: PropertyCollectionBuilder.whenAll([
PropertyCollectionBuilder.whenTrue('advanced'),
PropertyCollectionBuilder.whenGreaterThan<int>('capacity', 0),
])
// Less ideal: opaque function
visibleCondition: PropertyCollectionBuilder.whenCustom(
dependencies: ['advanced', 'capacity'],
evaluator: (c) => c.getValue<bool>('advanced') &&
c.getValue<int>('capacity') > 0,
)CustomCondition is appropriate when the logic involves multiple properties in ways the built-in conditions cannot express.
Derivation Best Practices
- Use
markDerivationsAsDirty()after loading saved data - Keep transformers pure (no side effects)
- Ensure source properties exist before targets
- Use
initiallyDirty: truefor derivations that should not run on form creation
Serialization Round-Trip
Test that toJson() and fromJson() produce consistent results:
final original = buildSettings();
original.setValue<String>('title', 'Test Course');
final json = original.toJson();
final restored = buildSettings();
restored.fromJson(json);
assert(original.toJson().toString() == restored.toJson().toString());Next Steps
- Course Settings Example -- Complete reference implementation
- Dashboard Config Example -- Dashboard widget configuration
- Architecture -- System design overview