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