2. Fields, Types, and Rows
A FieldType describes value interpretation. A Field<T> is a named, reusable token that carries that type plus storage, UI, seed, defaulting, and reference metadata.
Prefer semantic field tokens
abstract final class AreaFields {
static const code = CodeField();
static const name = NameField();
static const description = DescriptionField();
static const tenantId = TenantIdField();
static const siteId = SiteIdField();
}CodeField, NameField, and TenantIdField encode shared conventions. Use primitive tokens when the semantics are genuinely local:
static const capacity = IntegerField(
'capacity',
title: 'Capacity',
nullable: true,
);
static const roomNumber = TextField(
'room_number',
title: 'Room Number',
maxLength: 80,
);
static const calibratedAt = TimestampField(
'calibrated_at',
title: 'Calibrated At',
withTimeZone: true,
);Closed field types
The sealed interpretation core covers text, integers, big integers, doubles, exact decimals, booleans, timestamps, dates, intervals, UUIDs, JSONB, enums, and decisions. Higher-level tokens such as quantities, money, files, identifiers, and related fields compose those primitives.
Use DecimalField for regulated or financial precision. Its Dart boundary is a string so a binary floating-point conversion cannot silently change it.
Enums keep wire values stable and labels human
enum AreaType { room, suite, zone, corridor, airlock, wash_bay }
static const areaType = EnumField(
'area_type',
AreaType.values,
dbEnumName: 'area_type',
title: 'Area Type',
ui: FieldUI(
valueLabels: {'wash_bay': 'Wash Bay'},
),
);The persisted value is wash_bay; the UI renders Wash Bay. Filters, tables, forms, chips, and details all consume the same label map.
Defaults belong at the correct layer
static const id = UuidField(
'id',
nullable: false,
defaultExpr: FieldDefaultExpr.uuidV4,
);
static const createdAt = TimestampField(
'created_at',
nullable: false,
defaultExpr: FieldDefaultExpr.now,
);
static const createdBy = ActorField(
'created_by',
defaultExpr: FieldDefaultExpr.actorId,
);Postgres can own uuidV4 and now. The authenticated runtime injects actorId and siteId; they cannot be honest SQL defaults.
Reference fields carry picker semantics
static const areaCategoryId = UuidField(
'area_category_id',
title: 'Area Category',
indexed: true,
db: FieldDb(
queryPatterns: [QueryPattern.filter, QueryPattern.aggregate],
),
ui: FieldUI(
widget: UIFieldControl.referencePicker,
filterable: true,
),
reference: UIReferenceHint(
targetEntity: 'ops.area_category',
projection: 'picker',
),
);The storage value remains a UUID. The generated editor uses an entity picker and the generated detail surface uses an entity link. A user should not have to type or interpret the raw ID.
Typed row access
The same token reads and writes a loose storage row safely:
final row = Row(<String, Object?>{
'code': 'WASH-01',
'capacity': 12,
});
final code = row.require(AreaFields.code); // String
final capacity = row.maybe(AreaFields.capacity); // int?
row.set(AreaFields.name, 'Equipment Wash Bay');This is why field tokens must be reused rather than recreated from matching strings.
DB, UI, and seed hints do not change domain meaning
static const name = TextField(
'name',
maxLength: 240,
nullable: false,
db: FieldDb(
indexed: true,
queryPatterns: [QueryPattern.search, QueryPattern.sort],
),
ui: FieldUI(
searchable: true,
sortable: true,
order: 10,
),
seed: FieldSeed(
strategy: SeedValueStrategy.label,
examples: ['Equipment Wash Bay'],
),
);Hints guide interpreters. They do not create a second field definition.
Checkpoint
Create tokens for a controlled equipment record:
- code and name;
- serial number;
- category reference;
- decimal contact-surface area;
- multiple document attachments;
- lifecycle status enum.
Compare your result with the real Ops equipment.dart, then continue to Facets and entities.