Best Practices
Design patterns and recommendations for building robust applications
Best Practices and Patterns
This guide presents best practices, design patterns, and recommendations for building robust applications with the Vyuh Entity System. Following these guidelines will help you create maintainable, performant, and user-friendly applications.
Entity Design Best Practices
1. Entity Modeling
Keep Entities Focused
// ❌ Bad: Kitchen sink entity
class User extends EntityBase {
final String name;
final String email;
final List<Order> orders; // Should be a relationship
final List<Payment> payments; // Should be a relationship
final UserSettings settings; // Should be a separate entity
final List<ActivityLog> logs; // Should be queried separately
// ... 50 more fields
}
// ✅ Good: Focused entity with relationships
class User extends EntityBase {
final String name;
final String email;
final String? profileImageUrl;
final UserRole role;
final bool isActive;
// Relationships loaded separately
Future<List<Order>> get orders => OrderApi().getByUserId(id);
Future<UserSettings> get settings => UserSettingsApi().getByUserId(id);
}Use Meaningful Field Names
// ❌ Bad: Ambiguous names
class Product extends EntityBase {
final String n; // What is n?
final double p; // Price?
final int q; // Quantity?
final String stat; // Status?
}
// ✅ Good: Clear, descriptive names
class Product extends EntityBase {
final String name;
final double price;
final int stockQuantity;
final ProductStatus status;
}Implement Proper JSON Serialization
// ✅ Good: Complete serialization support
@JsonSerializable()
class Equipment extends EntityBase {
final String name;
final String serialNumber;
@JsonKey(name: 'location_id')
final String? locationId;
@DateTimeConverter()
final DateTime? lastCalibration;
@JsonKey(unknownEnumValue: EquipmentStatus.unknown)
final EquipmentStatus status;
// Custom converter for complex types
@SpecificationsConverter()
final Map<String, Specification> specifications;
factory Equipment.fromJson(Map<String, dynamic> json) =>
_$EquipmentFromJson(json);
@override
Map<String, dynamic> toJson() => _$EquipmentToJson(this);
}2. Configuration Patterns
Use Factory Methods for Complex Configurations
class UserConfig {
static EntityConfiguration<User> create({
required UserRole currentUserRole,
required FeatureFlags features,
}) {
return EntityConfiguration<User>(
metadata: _createMetadata(currentUserRole),
api: (client) => UserApi(client: client),
layouts: _createLayouts(currentUserRole, features),
form: _createForm(currentUserRole),
actions: _createActions(currentUserRole),
);
}
static EntityMetadata _createMetadata(UserRole role) {
return EntityMetadata(
identifier: 'users',
name: 'User',
pluralName: 'Users',
icon: Icons.person,
category: role == UserRole.admin ? 'Administration' : 'Organization',
route: EntityRouteBuilder.fromIdentifier('users'),
);
}
static EntityLayoutDescriptor<User> _createLayouts(
UserRole role,
FeatureFlags features,
) {
final layouts = <EntityLayout<User>>[];
// Always include table layout
layouts.add(UserTableLayout());
// Conditionally add layouts
if (features.isEnabled('user_grid_view')) {
layouts.add(UserGridLayout());
}
if (role == UserRole.admin) {
layouts.add(AdminUserLayout());
}
return EntityLayoutDescriptor(
list: layouts,
details: [UserDetailLayout()],
);
}
}Compose Configurations from Reusable Parts
// Base configuration traits
mixin AuditableEntityConfig<T extends EntityBase> on EntityConfiguration<T> {
@override
EntityActions<T> get actions => EntityActions<T>(
list: [
...super.actions?.list ?? [],
EntityAction(
label: 'View History',
icon: Icons.history,
onTap: (context, entities) => _showHistory(context, entities),
),
],
);
}
mixin ExportableEntityConfig<T extends EntityBase> on EntityConfiguration<T> {
@override
EntityActions<T> get actions => EntityActions<T>(
list: [
...super.actions?.list ?? [],
EntityAction(
label: 'Export',
icon: Icons.download,
onTap: (context, entities) => _exportEntities(context, entities),
),
],
);
}
// Composed configuration
class ProductConfiguration extends EntityConfiguration<Product>
with AuditableEntityConfig<Product>, ExportableEntityConfig<Product> {
// Product-specific configuration
}API Implementation Best Practices
1. Error Handling
Implement Comprehensive Error Handling
class RobustEntityApi<T extends EntityBase> extends EntityApi<T> {
@override
Future<List<T>> performList({
int? offset,
int? limit,
String? sortBy,
String? sortOrder,
String? search,
}) async {
try {
final response = await client.get(
'/api/v1/${T.toString().toLowerCase()}s',
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,
},
);
if (response.statusCode != 200) {
throw ApiException(
'Failed to load ${T.toString()}s',
statusCode: response.statusCode,
);
}
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => fromJson(json)).toList();
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout) {
throw NetworkException('Connection timeout');
} else if (e.type == DioExceptionType.badResponse) {
final message = e.response?.data['message'] ?? 'Server error';
throw ApiException(message, statusCode: e.response?.statusCode);
}
throw NetworkException('Network error: ${e.message}');
} catch (e) {
if (e is ApiException || e is NetworkException) rethrow;
throw ApiException('Unexpected error: $e');
}
}
}Use Result Types for Better Error Handling
// Result type for explicit error handling
sealed class Result<T> {
const Result();
factory Result.success(T value) = Success<T>;
factory Result.failure(String error, [Object? exception]) = Failure<T>;
R when<R>({
required R Function(T value) success,
required R Function(String error, Object? exception) failure,
});
}
class Success<T> extends Result<T> {
final T value;
const Success(this.value);
@override
R when<R>({
required R Function(T value) success,
required R Function(String error, Object? exception) failure,
}) => success(value);
}
class Failure<T> extends Result<T> {
final String error;
final Object? exception;
const Failure(this.error, [this.exception]);
@override
R when<R>({
required R Function(T value) success,
required R Function(String error, Object? exception) failure,
}) => failure(error, exception);
}
// Usage in API
class SafeEntityApi<T extends EntityBase> extends EntityApi<T> {
Future<Result<List<T>>> safeList({
int? offset,
int? limit,
}) async {
try {
final results = await list(offset: offset, limit: limit);
return Result.success(results);
} catch (e) {
return Result.failure('Failed to load data', e);
}
}
}
// Usage in UI
final result = await api.safeList();
result.when(
success: (entities) => _displayEntities(entities),
failure: (error, exception) => _showError(error),
);2. Caching Strategies
Implement Smart Caching
class CachedEntityApi<T extends EntityBase> extends EntityApi<T> {
final Duration cacheDuration;
final Map<String, CacheEntry<T>> _cache = {};
final Map<String, CacheEntry<List<T>>> _listCache = {};
CachedEntityApi({
required super.client,
this.cacheDuration = const Duration(minutes: 5),
});
@override
Future<T?> performGetById(String id) async {
// Check cache
final cached = _cache[id];
if (cached != null && !cached.isExpired) {
return cached.value;
}
// Load from API
final entity = await super.performGetById(id);
// Cache result
if (entity != null) {
_cache[id] = CacheEntry(
value: entity,
timestamp: DateTime.now(),
duration: cacheDuration,
);
}
return entity;
}
@override
Future<T> performUpdate(String id, T entity) async {
// Update via API
final updated = await super.performUpdate(id, entity);
// Invalidate caches
_cache.remove(id);
_listCache.clear(); // List results may have changed
// Cache new version
_cache[id] = CacheEntry(
value: updated,
timestamp: DateTime.now(),
duration: cacheDuration,
);
return updated;
}
void clearCache() {
_cache.clear();
_listCache.clear();
}
void invalidateEntity(String id) {
_cache.remove(id);
// Optionally invalidate related list caches
}
}
class CacheEntry<T> {
final T value;
final DateTime timestamp;
final Duration duration;
CacheEntry({
required this.value,
required this.timestamp,
required this.duration,
});
bool get isExpired =>
DateTime.now().difference(timestamp) > duration;
}Layout Best Practices
1. Responsive Design
Build Adaptive Layouts
class AdaptiveEntityLayout<T extends EntityBase> extends EntityLayout<T> {
@override
Widget build(BuildContext context, dynamic state) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return _buildMobileLayout(context, state);
} else if (constraints.maxWidth < 1200) {
return _buildTabletLayout(context, state);
} else {
return _buildDesktopLayout(context, state);
}
},
);
}
Widget _buildMobileLayout(BuildContext context, dynamic state) {
// Single column, vertical layout
return ListView.builder(
itemCount: state.entities.length,
itemBuilder: (context, index) => Card(
margin: const EdgeInsets.all(8),
child: EntityListTile(entity: state.entities[index]),
),
);
}
Widget _buildTabletLayout(BuildContext context, dynamic state) {
// Two column grid
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
),
itemCount: state.entities.length,
itemBuilder: (context, index) => EntityCard(
entity: state.entities[index],
),
);
}
Widget _buildDesktopLayout(BuildContext context, dynamic state) {
// Full table with all columns
return EntityDataTable(
entities: state.entities,
columns: _getAllColumns(),
);
}
}2. Performance Optimization
Use Virtual Scrolling for Large Lists
class VirtualizedListLayout<T extends EntityBase> extends EntityLayout<T> {
@override
Widget build(BuildContext context, EntityListViewState<T> state) {
return ScrollablePositionedList.builder(
itemCount: state.entities.length,
itemBuilder: (context, index) {
final entity = state.entities[index];
return EntityListItem(
key: ValueKey(entity.id),
entity: entity,
onTap: () => _handleEntityTap(context, entity),
);
},
itemScrollController: state.scrollController,
itemPositionsListener: state.positionsListener,
);
}
}Implement Efficient Search and Filter
class OptimizedSearchMixin<T extends EntityBase> {
Timer? _debounceTimer;
void onSearchChanged(
String query,
Function(String) performSearch,
) {
// Cancel previous timer
_debounceTimer?.cancel();
// Debounce search
_debounceTimer = Timer(
const Duration(milliseconds: 300),
() => performSearch(query),
);
}
List<T> filterEntities(
List<T> entities,
String query,
List<String Function(T)> searchFields,
) {
if (query.isEmpty) return entities;
final lowerQuery = query.toLowerCase();
return entities.where((entity) {
for (final field in searchFields) {
if (field(entity).toLowerCase().contains(lowerQuery)) {
return true;
}
}
return false;
}).toList();
}
}Form Best Practices
1. Form Validation
Implement Progressive Validation
class ProgressiveValidationForm extends StatefulWidget {
@override
State<ProgressiveValidationForm> createState() =>
_ProgressiveValidationFormState();
}
class _ProgressiveValidationFormState
extends State<ProgressiveValidationForm> {
final _formKey = GlobalKey<FormState>();
final _touchedFields = <String>{};
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) {
if (!_touchedFields.contains('email')) return null;
return _validateEmail(value);
},
onChanged: (value) {
setState(() => _touchedFields.add('email'));
if (_touchedFields.contains('email')) {
_formKey.currentState!.validate();
}
},
),
// More fields...
],
),
);
}
}Create Reusable Validators
class EntityValidators {
static FormFieldValidator<String> uniqueField({
required String fieldName,
required Future<bool> Function(String) checkUnique,
String? currentValue,
}) {
return (value) {
if (value == null || value.isEmpty) return null;
if (value == currentValue) return null; // No change
// This should be async, but FormFieldValidator doesn't support it
// Consider using a separate validation step
return null;
};
}
static FormFieldValidator<T> conditionalRequired<T>({
required bool Function() condition,
String? message,
}) {
return (value) {
if (!condition()) return null;
if (value == null || (value is String && value.isEmpty)) {
return message ?? 'This field is required';
}
return null;
};
}
static FormFieldValidator<String> matchesPattern({
required RegExp pattern,
required String message,
}) {
return (value) {
if (value == null || value.isEmpty) return null;
if (!pattern.hasMatch(value)) return message;
return null;
};
}
}2. Form State Management
Use Form Controllers Effectively
class EntityFormController extends ChangeNotifier {
final Map<String, dynamic> _data = {};
final Map<String, String> _errors = {};
final Set<String> _touchedFields = {};
bool _isSubmitting = false;
dynamic getValue(String field) => _data[field];
void setValue(String field, dynamic value) {
if (_data[field] != value) {
_data[field] = value;
_touchedFields.add(field);
notifyListeners();
}
}
String? getError(String field) => _errors[field];
void setError(String field, String? error) {
if (error == null) {
_errors.remove(field);
} else {
_errors[field] = error;
}
notifyListeners();
}
bool get isValid => _errors.isEmpty;
bool get isDirty => _touchedFields.isNotEmpty;
bool get isSubmitting => _isSubmitting;
Future<void> submit(
Future<void> Function(Map<String, dynamic>) onSubmit,
) async {
if (_isSubmitting) return;
_isSubmitting = true;
notifyListeners();
try {
await onSubmit(_data);
} finally {
_isSubmitting = false;
notifyListeners();
}
}
void reset() {
_data.clear();
_errors.clear();
_touchedFields.clear();
_isSubmitting = false;
notifyListeners();
}
}Permission Best Practices
1. Granular Permissions
Implement Field-Level Permissions
class FieldPermissions {
final Map<String, Permission> fieldPermissions;
FieldPermissions({required this.fieldPermissions});
bool canViewField(String field) {
final permission = fieldPermissions[field];
if (permission == null) return true; // No restriction
return PermissionService.hasPermission(permission);
}
bool canEditField(String field) {
final permission = fieldPermissions[field];
if (permission == null) return true; // No restriction
return PermissionService.hasPermission(
permission.copyWith(actions: [PermissionAction.update]),
);
}
Map<String, dynamic> filterViewableFields(Map<String, dynamic> data) {
return Map.fromEntries(
data.entries.where((entry) => canViewField(entry.key)),
);
}
}
// Usage in forms
class PermissionAwareForm extends StatelessWidget {
final FieldPermissions permissions;
@override
Widget build(BuildContext context) {
return Form(
child: Column(
children: [
if (permissions.canViewField('email'))
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
enabled: permissions.canEditField('email'),
),
if (permissions.canViewField('salary'))
NumberFormField(
decoration: const InputDecoration(labelText: 'Salary'),
enabled: permissions.canEditField('salary'),
),
],
),
);
}
}2. Permission Caching
Cache Permissions Efficiently
class CachedPermissionService extends EntityPermissionService {
static const _cacheKey = 'user_permissions';
static const _cacheDuration = Duration(minutes: 15);
@override
Future<Set<Permission>> _getPermissions() async {
// Try memory cache first
final cached = _memoryCache[userId];
if (cached != null && !cached.isExpired) {
return cached.permissions;
}
// Try persistent cache
final persistentCached = await _loadFromPersistentCache();
if (persistentCached != null) {
_memoryCache[userId] = persistentCached;
return persistentCached.permissions;
}
// Load from API
final permissions = await super._getPermissions();
// Cache results
final cacheEntry = PermissionCacheEntry(
permissions: permissions,
timestamp: DateTime.now(),
);
_memoryCache[userId] = cacheEntry;
await _saveToPersistentCache(cacheEntry);
return permissions;
}
void invalidateCache() {
_memoryCache.clear();
_clearPersistentCache();
}
}Testing Best Practices
1. Unit Testing
Test Entity Configurations
void main() {
group('UserConfiguration', () {
late EntityConfiguration<User> config;
setUp(() {
config = UserConfig.instance;
});
test('metadata is properly configured', () {
expect(config.metadata.identifier, 'users');
expect(config.metadata.name, 'User');
expect(config.metadata.pluralName, 'Users');
expect(config.metadata.icon, Icons.person);
});
test('api is properly configured', () {
final api = config.api(MockApiClient());
expect(api, isA<UserApi>());
});
test('layouts include required views', () {
expect(config.layouts.list, isNotEmpty);
expect(config.layouts.details, isNotEmpty);
// Check for specific layout types
expect(
config.layouts.list.any((l) => l is TableListLayout<User>),
isTrue,
reason: 'Should include table layout',
);
});
test('form descriptor creates valid forms', () {
final form = config.form.prepare(null);
expect(form.steps, isNotEmpty);
// Test form data transformation
final testUser = User(
id: '123',
name: 'Test User',
email: '[email protected]',
);
final formData = config.form.toFormData(testUser);
expect(formData['name'], 'Test User');
expect(formData['email'], '[email protected]');
final recreated = config.form.fromFormData(formData);
expect(recreated.name, testUser.name);
expect(recreated.email, testUser.email);
});
});
}Test API Implementations
void main() {
group('UserApi', () {
late UserApi api;
late MockApiClient mockClient;
setUp(() {
mockClient = MockApiClient();
api = UserApi(client: mockClient);
});
test('list returns users', () async {
when(mockClient.get(any, queryParameters: anyNamed('queryParameters')))
.thenAnswer((_) async => Response(
data: {
'data': [
{'id': '1', 'name': 'User 1'},
{'id': '2', 'name': 'User 2'},
],
},
statusCode: 200,
));
final users = await api.list();
expect(users, hasLength(2));
expect(users[0].name, 'User 1');
expect(users[1].name, 'User 2');
verify(mockClient.get('/api/v1/users', queryParameters: any));
});
test('handles errors properly', () async {
when(mockClient.get(any, queryParameters: anyNamed('queryParameters')))
.thenThrow(DioException(
type: DioExceptionType.badResponse,
response: Response(
statusCode: 404,
data: {'message': 'Not found'},
),
));
expect(
() => api.list(),
throwsA(isA<ApiException>()),
);
});
});
}2. Widget Testing
Test Entity Views
void main() {
group('EntityListView', () {
testWidgets('displays entities', (tester) async {
final mockProvider = MockEntityProvider<User>();
when(mockProvider.list()).thenAnswer((_) async => [
User(id: '1', name: 'User 1'),
User(id: '2', name: 'User 2'),
]);
await tester.pumpWidget(
MaterialApp(
home: Provider<EntityProvider<User>>.value(
value: mockProvider,
child: EntityListView<User>(
configuration: UserConfig.instance,
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('User 1'), findsOneWidget);
expect(find.text('User 2'), findsOneWidget);
});
testWidgets('handles empty state', (tester) async {
final mockProvider = MockEntityProvider<User>();
when(mockProvider.list()).thenAnswer((_) async => []);
await tester.pumpWidget(
MaterialApp(
home: Provider<EntityProvider<User>>.value(
value: mockProvider,
child: EntityListView<User>(
configuration: UserConfig.instance,
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('No users found'), findsOneWidget);
expect(find.byIcon(Icons.people_outline), findsOneWidget);
});
});
}Performance Best Practices
1. Optimize Entity Loading
Implement Lazy Loading
class LazyEntityProvider<T extends EntityBase> extends EntityProvider<T> {
final int pageSize;
final List<T> _loadedEntities = [];
bool _hasMore = true;
bool _isLoading = false;
LazyEntityProvider({
required super.configuration,
this.pageSize = 50,
});
@override
Future<List<T>> list() async {
if (_loadedEntities.isEmpty) {
await loadMore();
}
return _loadedEntities;
}
Future<void> loadMore() async {
if (_isLoading || !_hasMore) return;
_isLoading = true;
notifyListeners();
try {
final newEntities = await api.list(
offset: _loadedEntities.length,
limit: pageSize,
);
_loadedEntities.addAll(newEntities);
_hasMore = newEntities.length == pageSize;
} finally {
_isLoading = false;
notifyListeners();
}
}
void reset() {
_loadedEntities.clear();
_hasMore = true;
_isLoading = false;
notifyListeners();
}
}2. Optimize Rendering
Use Keys Properly
class OptimizedEntityList<T extends EntityBase> extends StatelessWidget {
final List<T> entities;
@override
Widget build(BuildContext context) {
return ListView.builder(
// Use item extent for better performance
itemExtent: 72.0,
itemCount: entities.length,
itemBuilder: (context, index) {
final entity = entities[index];
return EntityListTile(
// Use value key for stable identity
key: ValueKey(entity.id),
entity: entity,
);
},
);
}
}Common Pitfalls and Solutions
1. Memory Leaks
Dispose Resources Properly
class EntityViewState<T extends EntityBase> extends State<EntityView<T>> {
late StreamSubscription<List<T>> _subscription;
late Timer _refreshTimer;
@override
void initState() {
super.initState();
_subscription = widget.provider.stream.listen(_onEntitiesChanged);
_refreshTimer = Timer.periodic(
const Duration(minutes: 1),
(_) => _refresh(),
);
}
@override
void dispose() {
_subscription.cancel();
_refreshTimer.cancel();
super.dispose();
}
}2. Race Conditions
Handle Concurrent Operations
class SafeEntityOperations<T extends EntityBase> {
final _operationLocks = <String, Future<void>>{};
Future<T> safeUpdate(String id, T Function(T) updater) async {
// Wait for any existing operation on this entity
final existingOperation = _operationLocks[id];
if (existingOperation != null) {
await existingOperation;
}
// Create new operation
final completer = Completer<void>();
_operationLocks[id] = completer.future;
try {
// Perform update
final current = await api.byId(id);
if (current == null) throw NotFoundException();
final updated = updater(current);
final result = await api.update(id, updated);
return result;
} finally {
completer.complete();
_operationLocks.remove(id);
}
}
}Summary
Following these best practices will help you build robust, maintainable applications with the Vyuh Entity System:
- Design entities thoughtfully - Keep them focused and well-structured
- Handle errors gracefully - Provide meaningful feedback to users
- Optimize performance - Use caching, lazy loading, and virtual scrolling
- Test thoroughly - Unit test configurations, APIs, and UI components
- Manage state carefully - Avoid memory leaks and race conditions
- Follow platform conventions - Ensure consistency across your application
Next: Examples - Complete examples from production features