Vyuh CDX

Error Handling

Standardized error handling, loading states, and empty states patterns

Entity System Error Handling Best Practices

Overview

The Vyuh Entity System provides standardized state widgets for consistent error handling, loading states, and empty states across all entity views. All entity layouts should use these standardized widgets instead of implementing custom error handling.

Standardized State Widgets

1. EntityLoadingState<T>

Use for showing loading indicators with entity context.

if (_isLoading.value) {
  return const EntityLoadingState<Equipment>(
    message: 'Loading equipment data...', // Optional custom message
    showEntityContext: true, // Shows entity icon and name
  );
}

2. EntityErrorState<T>

Use for displaying errors with retry functionality.

if (_error.value != null) {
  return EntityErrorState<Equipment>(
    error: _error.value,
    title: 'Error loading equipment', // Optional custom title
    onRetry: _fetchData, // Retry callback
    showRetryButton: true, // Show retry button (default: true)
  );
}

3. EntityEmptyState<T>

Use for empty data states.

if (_items.isEmpty) {
  return const EntityEmptyState<Equipment>(
    message: 'No equipment found', // Optional custom message
    description: 'Create your first equipment to get started', // Optional description
    showCreateButton: true, // Show create button (default: true)
    onCreatePressed: null, // Optional custom create handler
  );
}

Implementation Pattern

Here's the recommended pattern for entity layouts:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:shared_api_client/shared_api_client.dart';
import 'package:vyuh_core/vyuh_core.dart';
import 'package:vyuh_entity_system/vyuh_entity_system.dart';

class MyEntityLayout extends LayoutConfiguration<MyEntity> {
  @override
  Widget build(BuildContext context, MyEntity entity) {
    return _MyEntityView(entity: entity);
  }
}

class _MyEntityView extends StatefulWidget {
  final MyEntity entity;
  
  const _MyEntityView({required this.entity});
  
  @override
  State<_MyEntityView> createState() => _MyEntityViewState();
}

class _MyEntityViewState extends State<_MyEntityView> {
  late final ObservableList<dynamic> _items = ObservableList<dynamic>();
  final Observable<bool> _isLoading = Observable(true);
  final Observable<String?> _error = Observable(null);
  
  @override
  void initState() {
    super.initState();
    _fetchData();
  }
  
  Future<void> _fetchData() async {
    try {
      runInAction(() {
        _isLoading.value = true;
        _error.value = null;
      });
      
      // Fetch data...
      final api = vyuh.di.get<SharedApiClient>();
      final response = await api.get('/api/endpoint');

      if (response.statusCode != 200) {
        throw Exception('Failed to load data: ${response.statusCode}');
      }

      final responseData = jsonDecode(response.body);
      if (responseData['success'] != true) {
        throw Exception('API error: ${responseData['error']}');
      }

      runInAction(() {
        _items.clear();
        _items.addAll(responseData['data'] as List);
        _isLoading.value = false;
      });
    } catch (e) {
      runInAction(() {
        _error.value = e.toString();
        _isLoading.value = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Observer(
      builder: (_) {
        // Use standardized loading state
        if (_isLoading.value) {
          return const EntityLoadingState<MyEntity>(
            message: 'Loading data...',
          );
        }
        
        // Use standardized error state
        if (_error.value != null) {
          return EntityErrorState<MyEntity>(
            error: _error.value,
            title: 'Error loading data',
            onRetry: _fetchData,
          );
        }
        
        // Use standardized empty state
        if (_items.isEmpty) {
          return const EntityEmptyState<MyEntity>(
            message: 'No data found',
            description: 'Add some data to see it here',
            showCreateButton: false,
          );
        }
        
        // Normal content
        return ListView.builder(
          // ... your list implementation
        );
      },
    );
  }
}

Benefits

  1. Consistency: All entity views have the same look and feel for loading, error, and empty states
  2. Maintainability: Changes to error handling can be made in one place
  3. Entity Awareness: The widgets automatically use entity metadata (icon, name, theme color)
  4. Better UX: Users get consistent, professional error messages with clear actions
  5. Accessibility: Standardized widgets include proper accessibility support

Migration Guide

To migrate existing layouts:

  1. Add import: import 'package:vyuh_entity_system/vyuh_entity_system.dart';
  2. Replace Center(child: CircularProgressIndicator()) with EntityLoadingState<T>
  3. Replace custom error UI with EntityErrorState<T>
  4. Replace custom empty state UI with EntityEmptyState<T>
  5. Ensure the generic type parameter matches your entity type

Common Mistakes to Avoid

  1. Don't use custom error UI: Always use the standardized widgets
  2. Don't forget the generic type: Always specify the entity type (e.g., EntityErrorState<Equipment>)
  3. Don't hide retry buttons: Unless there's a specific reason, always provide retry functionality
  4. Don't use generic messages: Provide context-specific messages when possible

Examples from the Codebase

Good examples of standardized error handling can be found in:

  • equipment_areas_layout.dart
  • equipment_user_groups_layout.dart
  • area_user_groups_layout.dart
  • user_areas_layout.dart
  • user_equipment_layout.dart
  • location_users_layout.dart