Entity Configuration
Complete guide to EntityConfiguration class and bringing all entity aspects together
The EntityConfiguration class is the heart of the Vyuh Entity System. It
brings together all aspects of an entity - from metadata and API to layouts and
forms - into a single, cohesive configuration. This guide provides an in-depth
look at every aspect of entity configuration.
Overview
class EntityConfiguration<T extends EntityBase> {
final EntityMetadata metadata;
final EntityApi<T> api;
final EntityLayoutDescriptor<T> layouts;
final EntityFormDescriptor<T> form;
final EntityActions<T>? actions;
final RouteBuilder? routeBuilder;
// ... constructor and methods
}Core Components
1. EntityMetadata
The metadata describes your entity and controls how it appears throughout the system.
class EntityMetadata {
final String identifier; // Unique identifier (e.g., 'users', 'products')
final String name; // Singular display name
final String pluralName; // Plural display name
final String? description; // Entity description
final IconData? icon; // Display icon
final Color? themeColor; // Theme color for UI elements
final String? category; // Menu category
final EntityRouteBuilder route; // Route configuration
final Future<String?> Function(BuildContext)? getHelp; // Help provider
}Example Configuration
EntityMetadata(
identifier: 'reference_standards',
name: 'Reference Standard',
pluralName: 'Reference Standards',
description: 'Chemical and biological reference materials',
icon: Icons.science,
themeColor: Colors.purple,
category: 'Laboratory',
route: EntityRouteBuilder.fromIdentifier('reference-standards'),
getHelp: (context) async {
return '''
Reference standards are certified materials used for:
- Instrument calibration
- Method validation
- Quality control
''';
},
)2. API Configuration
The API configuration provides a factory function that creates an EntityApi
instance:
api: (client) => ReferenceStandardApi(client: client),Your API class should extend EntityApi<T>:
class ReferenceStandardApi extends EntityApi<ReferenceStandard> {
ReferenceStandardApi({required super.client});
@override
ReferenceStandard fromJson(Map<String, dynamic> json) =>
ReferenceStandard.fromJson(json);
// Optional: Override base methods for custom behavior
@override
Future<List<ReferenceStandard>> performList({
int? offset,
int? limit,
String? sortBy,
String? sortOrder,
String? search,
}) async {
// Custom list implementation
final response = await client.get(
'/api/v1/reference-standards',
queryParameters: {
if (offset != null) 'offset': offset.toString(),
if (limit != null) 'limit': limit.toString(),
if (sortBy != null) 'sortBy': sortBy,
if (sortOrder != null) 'sortOrder': sortOrder,
if (search != null) 'search': search,
},
);
final List<dynamic> data = response.data['data'];
return data.map((json) => fromJson(json)).toList();
}
}3. Layout Configuration
The EntityLayoutDescriptor defines how your entity is displayed in different
contexts:
class EntityLayoutDescriptor<T> {
final List<EntityLayout<T>> list; // List view layouts
final List<EntityLayout<T>> details; // Detail view layouts
final List<EntityLayout<T>>? summary; // Summary/card layouts
final List<EntityLayout<T>>? dashboard; // Dashboard view layouts
final List<EntityLayout<T>>? analytics; // Analytics contributions
}List Layouts
list: [
// Table layout for desktop
TableListLayout<User>(
title: 'Users',
columns: [
TableColumn(
key: 'name',
label: 'Name',
getValue: (user) => user.name,
sortable: true,
searchable: true,
),
TableColumn(
key: 'email',
label: 'Email',
getValue: (user) => user.email,
sortable: true,
),
TableColumn(
key: 'role',
label: 'Role',
getValue: (user) => user.role.toUpperCase(),
width: 100,
),
TableColumn(
key: 'status',
label: 'Status',
getValue: (user) => user.isActive ? 'Active' : 'Inactive',
buildCell: (context, user) => Chip(
label: Text(user.isActive ? 'Active' : 'Inactive'),
backgroundColor: user.isActive ? Colors.green : Colors.grey,
),
),
],
onRowTap: (context, user) {
// Navigate to detail view
context.go('/users/${user.id}');
},
bulkActions: [
BulkAction(
label: 'Activate',
icon: Icons.check,
onTap: (context, users) async {
// Bulk activate logic
},
),
],
),
// Grid layout for mobile/tablet
GridListLayout<User>(
title: 'Users',
crossAxisCount: 2,
buildCard: (context, user) => Card(
child: InkWell(
onTap: () => context.go('/users/${user.id}'),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
child: Text(user.name[0]),
),
const SizedBox(height: 8),
Text(
user.name,
style: Theme.of(context).textTheme.titleMedium,
),
Text(
user.email,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
),
),
]Detail Layouts
details: [
// Standard detail layout
EntityLayoutConfiguration<User>(
canHandle: (context) => true, // Always use this layout
build: (context, user) => SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header section
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 40,
child: Text(user.name[0]),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.name,
style: Theme.of(context).textTheme.headlineMedium,
),
Text(user.email),
Chip(
label: Text(user.role),
),
],
),
),
],
),
),
),
// Information sections
const SizedBox(height: 24),
_buildSection(
context,
title: 'Contact Information',
children: [
_buildInfoRow('Phone', user.phone),
_buildInfoRow('Department', user.department),
_buildInfoRow('Location', user.location),
],
),
// Related entities
const SizedBox(height: 24),
_buildSection(
context,
title: 'Permissions',
children: [
FutureBuilder<List<Permission>>(
future: _loadUserPermissions(user.id),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
return Wrap(
spacing: 8,
children: snapshot.data!.map((permission) {
return Chip(label: Text(permission.name));
}).toList(),
);
},
),
],
),
],
),
),
),
]4. Form Configuration
The EntityFormDescriptor handles form creation and data transformation:
abstract class EntityFormDescriptor<T> {
StepForm prepare(T? entity);
Map<String, dynamic> toFormData(T entity);
T fromFormData(Map<String, dynamic> data, {T? entity});
}Complex Form Example
class UserFormDescriptor extends EntityFormDescriptor<User> {
@override
StepForm prepare(User? entity) {
final isEdit = entity != null;
return StepForm(
title: isEdit ? 'Edit User' : 'Create User',
steps: [
FormStep(
title: 'Basic Information',
form: FormBuilder(
title: 'User Details',
fields: [
TextField(
name: 'name',
label: 'Full Name',
value: entity?.name,
validators: [
required(),
minLength(3),
maxLength(100),
],
),
TextField(
name: 'email',
label: 'Email Address',
value: entity?.email,
validators: [
required(),
email(),
// Custom async validator
AsyncValidator(
validator: (value) async {
if (isEdit && value == entity.email) return null;
final exists = await checkEmailExists(value);
return exists ? 'Email already in use' : null;
},
),
],
),
SelectField(
name: 'role',
label: 'Role',
value: entity?.role,
options: [
Option(value: 'admin', label: 'Administrator'),
Option(value: 'manager', label: 'Manager'),
Option(value: 'user', label: 'Standard User'),
],
validators: [required()],
),
],
),
),
FormStep(
title: 'Department & Location',
form: FormBuilder(
title: 'Assignment',
fields: [
SelectField(
name: 'department',
label: 'Department',
value: entity?.department,
optionsBuilder: () async {
final departments = await loadDepartments();
return departments.map((d) =>
Option(value: d.id, label: d.name)
).toList();
},
validators: [required()],
),
SelectField(
name: 'location',
label: 'Location',
value: entity?.location,
dependsOn: ['department'],
optionsBuilder: (formData) async {
final deptId = formData['department'];
if (deptId == null) return [];
final locations = await loadLocations(deptId);
return locations.map((l) =>
Option(value: l.id, label: l.name)
).toList();
},
),
TextField(
name: 'phone',
label: 'Phone Number',
value: entity?.phone,
validators: [
pattern(r'^\+?[\d\s-()]+$', 'Invalid phone number'),
],
),
],
),
),
FormStep(
title: 'Permissions',
form: FormBuilder(
title: 'Access Control',
fields: [
MultiSelectField(
name: 'permissions',
label: 'Permissions',
value: entity?.permissions,
optionsBuilder: () async {
final permissions = await loadAvailablePermissions();
return permissions.map((p) =>
Option(
value: p.id,
label: p.name,
description: p.description,
)
).toList();
},
),
ToggleField(
name: 'isActive',
label: 'Active Account',
value: entity?.isActive ?? true,
description: 'Inactive accounts cannot log in',
),
],
),
),
],
);
}
@override
Map<String, dynamic> toFormData(User entity) {
return {
'name': entity.name,
'email': entity.email,
'role': entity.role,
'department': entity.department,
'location': entity.location,
'phone': entity.phone,
'permissions': entity.permissions,
'isActive': entity.isActive,
};
}
@override
User fromFormData(Map<String, dynamic> data, {User? entity}) {
return User(
id: entity?.id ?? '',
schemaType: 'users',
name: data['name'],
email: data['email'],
role: data['role'],
department: data['department'],
location: data['location'],
phone: data['phone'],
permissions: List<String>.from(data['permissions'] ?? []),
isActive: data['isActive'],
createdAt: entity?.createdAt ?? DateTime.now(),
);
}
}5. Entity Actions
Actions provide custom operations for single entities or bulk operations:
class EntityActions<T> {
final List<EntityAction<T>>? list; // Actions for list view
final List<EntityAction<T>>? single; // Actions for single entity
final List<EntityAction<T>>? bulk; // Bulk actions
}
class EntityAction<T> {
final String label;
final IconData? icon;
final Future<void> Function(BuildContext, List<T>) onTap;
final bool Function(List<T>)? isEnabled;
final bool requiresConfirmation;
}Action Examples
actions: EntityActions<Equipment>(
list: [
EntityAction(
label: 'Import CSV',
icon: Icons.upload_file,
onTap: (context, _) async {
final file = await pickFile();
if (file != null) {
await importEquipmentFromCsv(file);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Import completed')),
);
}
}
},
),
],
single: [
EntityAction(
label: 'Generate QR Code',
icon: Icons.qr_code,
onTap: (context, entities) async {
final equipment = entities.first;
final qrCode = await generateQrCode(equipment);
if (context.mounted) {
showDialog(
context: context,
builder: (_) => QrCodeDialog(qrCode: qrCode),
);
}
},
),
EntityAction(
label: 'Schedule Maintenance',
icon: Icons.calendar_today,
isEnabled: (entities) =>
entities.first.status == EquipmentStatus.operational,
onTap: (context, entities) async {
final equipment = entities.first;
await showMaintenanceScheduler(context, equipment);
},
),
],
bulk: [
EntityAction(
label: 'Assign to Location',
icon: Icons.location_on,
requiresConfirmation: true,
onTap: (context, entities) async {
final location = await showLocationPicker(context);
if (location != null) {
await assignEquipmentToLocation(entities, location);
}
},
),
],
),6. Custom Route Builder
For advanced routing needs, provide a custom route builder:
routeBuilder: (configuration) {
return (BuildContext context, GoRouterState state) {
// Custom route handling
final entityId = state.pathParameters['id'];
// Example: Add animation
return CustomTransitionPage(
child: EntityDetailView<Equipment>(
configuration: configuration,
entityId: entityId!,
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
);
};
},Complete Configuration Example
Here's a comprehensive example bringing all components together:
class LocationConfig {
static final instance = EntityConfiguration<Location>(
metadata: EntityMetadata(
identifier: 'locations',
name: 'Location',
pluralName: 'Locations',
description: 'Physical locations and facilities',
icon: Icons.location_city,
themeColor: Colors.teal,
category: 'Organization',
route: EntityRouteBuilder.fromIdentifier('locations'),
getHelp: (context) async =>
await loadHelpContent('locations'),
),
api: (client) => LocationApi(client: client),
layouts: EntityLayoutDescriptor(
list: [
TableListLayout<Location>(
title: 'Locations',
columns: _buildTableColumns(),
filters: _buildFilters(),
defaultSort: 'name',
),
LocationMapLayout(), // Custom map view
],
details: [
LocationDetailLayout(),
LocationFloorPlanLayout(), // Custom floor plan view
],
summary: [
LocationCardLayout(),
],
dashboard: [
LocationStatsWidget(),
LocationUtilizationChart(),
],
),
form: LocationFormDescriptor(),
actions: EntityActions<Location>(
list: [
EntityAction(
label: 'Import Locations',
icon: Icons.upload,
onTap: _handleImport,
),
],
single: [
EntityAction(
label: 'View Floor Plan',
icon: Icons.map,
onTap: _showFloorPlan,
),
EntityAction(
label: 'Generate Report',
icon: Icons.description,
onTap: _generateReport,
),
],
),
);
}Advanced Configuration Patterns
Dynamic Configuration
Configuration can be dynamic based on user permissions or context:
EntityConfiguration<Document>(
// ... other config
layouts: EntityLayoutDescriptor(
list: [
// Show different layouts based on user role
if (userRole == 'admin')
AdminDocumentListLayout()
else
StandardDocumentListLayout(),
],
),
actions: EntityActions(
single: [
// Conditionally add actions
if (hasPermission('documents.approve'))
EntityAction(
label: 'Approve',
icon: Icons.check,
onTap: _approveDocument,
),
],
),
)Configuration Composition
Build configurations from reusable components:
class BaseEntityConfig {
static EntityLayoutDescriptor<T> createStandardLayouts<T>() {
return EntityLayoutDescriptor<T>(
list: [
TableListLayout<T>(...),
GridListLayout<T>(...),
],
details: [
StandardDetailLayout<T>(),
],
);
}
static EntityActions<T> createStandardActions<T>() {
return EntityActions<T>(
list: [
EntityAction(
label: 'Export',
icon: Icons.download,
onTap: (context, entities) => exportEntities(entities),
),
],
);
}
}
// Use in specific configurations
final config = EntityConfiguration<Product>(
// ... metadata and api
layouts: BaseEntityConfig.createStandardLayouts<Product>(),
actions: BaseEntityConfig.createStandardActions<Product>(),
form: ProductFormDescriptor(),
);Best Practices
- Keep configurations focused - Each configuration should handle one entity type
- Use type safety - Leverage Dart's type system for compile-time safety
- Provide multiple layouts - Different users prefer different views
- Include help content - Use the
getHelpfunction for context-sensitive help - Test configurations - Unit test form descriptors and API implementations
- Document custom behaviors - Comment any non-standard implementations
Next: Entity Metadata - Deep dive into entity metadata configuration