Collections
A PropertyCollection groups multiple properties into a single manageable unit with shared validation, condition evaluation, serialization, and UI rendering.
PropertyCollection
The core container for properties. Internally it builds a reactive_forms FormGroup from all property controls and listens to value changes for condition evaluation.
final collection = PropertyCollection(
groups: [
PropertyGroup.collapsible(value: 'basic', title: 'Basic Info'),
PropertyGroup.collapsible(value: 'advanced', title: 'Advanced'),
],
properties: [
StringProperty(key: 'title', label: 'Title', group: 'basic', required: true),
StringProperty(key: 'description', label: 'Description', group: 'basic'),
IntProperty(key: 'timeout', label: 'Timeout (sec)', group: 'advanced', defaultValue: 30),
],
);Key Operations
| Operation | Method | Description |
|---|---|---|
| Read value | getValue<T>(key) | Type-safe value access |
| Write value | setValue<T>(key, value) | Type-safe value set |
| Get property | getProperty(key) | Get the Property instance |
| Check existence | hasProperty(key) | Whether key exists |
| All properties | properties | List in insertion order |
| By group | getPropertiesByGroup(value) | Properties in a group |
| Validate | validateAll() | Returns Map<String, String> of errors |
| Is valid | isValid | True if all properties pass validation |
| Has changes | hasChanges | True if any value differs from default |
| Serialize | toJson() | All property values as Map<String, dynamic> |
| Deserialize | fromJson(json) | Restore values from JSON map |
| Reset | resetAll() | Reset all properties to defaults |
| Clone | clone() | Deep clone the entire collection |
| Copy from | copyFrom(other) | Replace contents from another collection |
| Dispose | dispose() | Cancel all subscriptions |
Value Streams
// Stream of all value changes
collection.valueChanges.listen((values) {
print('Values changed: $values');
});
// Current values snapshot
final Map<String, dynamic> current = collection.values;PropertyGroup
Groups provide visual organization in the editor. There are three factory constructors:
Collapsible Group
Renders as an ExpansionTile that the user can expand/collapse:
PropertyGroup.collapsible(
value: 'advanced',
title: 'Advanced Settings',
initiallyCollapsed: true, // start collapsed
);Fixed Group
Renders as a non-collapsible section with a header:
PropertyGroup.fixed(
value: 'metadata',
title: 'Metadata',
showHeader: true,
);General Group
Auto-created for ungrouped properties. You rarely need to create this manually:
PropertyGroup.general(
title: 'General',
showHeader: false, // no header when it's the only group
);PropertyGroup Fields
| Field | Type | Default | Description |
|---|---|---|---|
value | String | Required | Unique identifier |
title | String | Required | Display title |
collapsible | bool | true | Whether the group can be collapsed |
initiallyCollapsed | bool | false | Whether to start collapsed |
showHeader | bool | true | Whether to show the group header |
PropertyCollectionBuilder
The fluent builder API eliminates boilerplate when constructing collections. It tracks groups, properties, and derivations, then builds them into a PropertyCollection.
Basic Usage
final b = PropertyCollectionBuilder();
// Start a group context
b.group('basic', 'Basic Info');
b.string('title', 'Title', required: true);
b.string('description', 'Description');
b.integer('duration', 'Duration', defaultValue: 60);
// Start another group
b.group('enrollment', 'Enrollment');
b.boolean('open', 'Open Enrollment', defaultValue: true);
b.integer('capacity', 'Capacity', defaultValue: 30,
visibleCondition: PropertyCollectionBuilder.whenTrue('open'));
// Build the collection
final collection = b.buildCollection();Properties declared after a group() call are automatically assigned to that group. Use endGroup() to return to ungrouped:
b.group('settings', 'Settings');
b.string('theme', 'Theme');
b.endGroup();
b.string('notes', 'Notes'); // ungroupedBuilder Methods
| Method | Returns | Description |
|---|---|---|
group(value, title, ...) | PropertyCollectionBuilder | Start a group context |
endGroup() | PropertyCollectionBuilder | End current group context |
string(key, label, ...) | StringProperty | Add a string property |
optionalText(key, label, ...) | OptionalStringProperty | Add a nullable string |
integer(key, label, ...) | IntProperty | Add an integer property |
optionalInteger(key, label, ...) | OptionalIntProperty | Add a nullable integer |
decimal(key, label, ...) | DoubleProperty | Add a double property |
optionalDecimal(key, label, ...) | OptionalDoubleProperty | Add a nullable double |
boolean(key, label, ...) | BoolProperty | Add a boolean property |
optionalBoolean(key, label, ...) | OptionalBoolProperty | Add a nullable boolean |
dateTime(key, label, ...) | DateTimeProperty | Add a DateTime property |
optionalDateTime(key, label, ...) | OptionalDateTimeProperty | Add a nullable DateTime |
enumeration<T>(key, label, ...) | EnumProperty<T> | Add an enum property |
list<T>(key, label, ...) | ListProperty<T> | Add a list property |
readonly<T>(key, label, ...) | ReadOnlyProperty<T> | Add a read-only property |
derivedProperty(target, ...) | PropertyCollectionBuilder | Add a derivation rule |
buildCollection() | PropertyCollection | Build the final collection |
buildList() | List<Property> | Build just the property list |
groups | List<PropertyGroup> | Get configured groups |
Condition Convenience Methods
The builder provides static methods for creating conditions inline:
PropertyCollectionBuilder.when<T>(key, value) // EqualsCondition
PropertyCollectionBuilder.whenNot<T>(key, value) // NotEqualsCondition
PropertyCollectionBuilder.whenIn<T>(key, values) // InCondition
PropertyCollectionBuilder.whenNotIn<T>(key, values) // NotInCondition
PropertyCollectionBuilder.whenTrue(key) // EqualsCondition<bool>(true)
PropertyCollectionBuilder.whenFalse(key) // EqualsCondition<bool>(false)
PropertyCollectionBuilder.whenHasValue(key) // HasValueCondition
PropertyCollectionBuilder.whenAll(conditions) // AndCondition
PropertyCollectionBuilder.whenAny(conditions) // OrCondition
PropertyCollectionBuilder.whenGreaterThan<T>(key, v) // ComparisonCondition
PropertyCollectionBuilder.whenLessThan<T>(key, v) // ComparisonCondition
PropertyCollectionBuilder.whenCustom(deps, fn) // CustomConditionPropertyCollectionEditor
The editor widget renders a PropertyCollection as a form. It supports grouped and ungrouped layouts, scrolling, and shrink-wrapping.
PropertyCollectionEditor(
collection: collection,
showGroups: true, // render groups with headers
shrinkWrap: true, // wrap content height
scrollable: true, // enable scrolling
disposeCollection: true, // dispose collection on widget dispose
onChanged: () => print('Changed'),
);You can also use the collection's convenience method:
collection.createEditor(
context: context,
showGroups: true,
shrinkwrap: true,
scrollable: true,
);Single-Line Editor
For compact layouts, render all editable properties in a horizontal row:
collection.createSingleLineEditor(
context: context,
flexValues: [2, 1, 1], // optional flex weights
);How Rendering Works
- The editor builds a flat list of items (properties and group headers)
- Each property is wrapped in an
_IsolatedPropertyEditorthat usesValueListenableBuilder<bool>on the property'svisibleNotifier - When visibility changes, only the affected property rebuilds -- not the entire form
- Groups render as
ExpansionTile(collapsible) orColumn(fixed)
PropertyDerivation
A derivation auto-computes a target property's value from one or more source properties:
b.string('title', 'Title', required: true);
b.string('slug', 'URL Slug', required: true);
// Auto-derive slug from title
b.derivedProperty('slug',
from: {'title'},
transformer: (values) =>
PropertyValidators.toIdentifier(values['title'] as String?),
);Behavior
- When a source property changes, the transformer runs and updates the target
- If the user manually edits the target, auto-derivation stops (marked dirty)
- If the user clears the target, auto-derivation resumes
- Set
initiallyDirty: trueto prevent derivation when loading existing data
PropertyDerivation Fields
| Field | Type | Description |
|---|---|---|
targetProperty | String | Key of the property to auto-compute |
sourceProperties | Set<String> | Keys of properties to watch |
transformer | Function(Map<String, dynamic>) | Computes the new value from all property values |
initiallyDirty | bool | Whether to skip derivation initially |
Adding Derivations
Derivations can be added via the builder or directly on a collection:
// Via builder
b.derivedProperty('slug', from: {'title'}, transformer: (v) => ...);
final collection = b.buildCollection();
// Directly on collection
collection.addDerivations([
PropertyDerivation(
targetProperty: 'slug',
sourceProperties: {'title'},
transformer: (values) => ...,
),
]);To mark all derivations as dirty (e.g., after loading data):
collection.markDerivationsAsDirty();Next Steps
- Conditions -- All condition types for reactive behavior
- Validation -- Validators in detail
- Building Forms Guide -- Complete form-building walkthrough