Vyuh CDX

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:

  1. Design entities thoughtfully - Keep them focused and well-structured
  2. Handle errors gracefully - Provide meaningful feedback to users
  3. Optimize performance - Use caching, lazy loading, and virtual scrolling
  4. Test thoroughly - Unit test configurations, APIs, and UI components
  5. Manage state carefully - Avoid memory leaks and race conditions
  6. Follow platform conventions - Ensure consistency across your application

Next: Examples - Complete examples from production features