Entity Configuration
Complete guide to EntityConfiguration class and bringing all entity aspects together
Entity Configuration - Complete Guide
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> Function(SharedApiClient) 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: Metadata API - Deep dive into entity metadata and API patterns