Skip to content

Best Practices ​

Guidelines for designing effective property collections.

Property Key Naming ​

Use consistent, descriptive keys in camelCase:

dart
// 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');  // abbreviations

Group Organization ​

Organize properties into logical groups. Place the most commonly edited properties in the first group, and less frequently changed settings in later groups:

dart
b.group('basic', 'Basic Information');  // most common
b.group('enrollment', 'Enrollment');    // important but secondary
b.group('advanced', 'Advanced',
  initiallyCollapsed: true);            // rarely changed

Use initiallyCollapsed: true for advanced or rarely-used groups.

Validation Placement ​

Prefer adding validators at property declaration time rather than validating at save time:

dart
// 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:

dart
// 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:

dart
// 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:

dart
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:

dart
// 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: true for derivations that should not run on form creation

Serialization Round-Trip ​

Test that toJson() and fromJson() produce consistent results:

dart
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 ​