Vyuh CDX
Configuration

Layouts & Views

Flexible layout system for creating multiple views for your entities

The Vyuh Entity System provides a powerful and flexible layout system that allows you to create multiple views for your entities. This guide covers all aspects of creating, configuring, and using layouts.

Layout System Overview

The layout system is built on these core concepts:

  1. Layout Descriptors - Define available layouts for each view type
  2. Layout Implementations - Actual widget implementations
  3. Layout Selection - Logic to choose appropriate layouts
  4. Layout Configuration - Customization options

EntityLayoutDescriptor

The EntityLayoutDescriptor organizes layouts by view type:

class EntityLayoutDescriptor<T extends EntityBase> {
  final List<EntityLayout<T>> list;          // List view layouts
  final List<EntityLayout<T>> details;       // Detail view layouts
  final List<EntityLayout<T>>? summary;      // Card/summary layouts
  final List<EntityLayout<T>>? dashboard;    // Dashboard widgets
  final List<EntityLayout<T>>? analytics;    // Analytics views
}

Built-in Layouts

TableListLayout

The most common list layout for desktop views:

TableListLayout<User>(
  title: 'Users',
  columns: [
    TableColumn<User>(
      key: 'name',
      label: 'Name',
      getValue: (user) => user.name,
      width: 200,
      sortable: true,
      searchable: true,
      buildCell: (context, user) => Row(
        children: [
          CircleAvatar(
            radius: 16,
            child: Text(user.name[0]),
          ),
          const SizedBox(width: 8),
          Expanded(child: Text(user.name)),
        ],
      ),
    ),
    TableColumn<User>(
      key: 'email',
      label: 'Email',
      getValue: (user) => user.email,
      sortable: true,
      copyable: true,
    ),
    TableColumn<User>(
      key: 'status',
      label: 'Status',
      getValue: (user) => user.isActive ? 'Active' : 'Inactive',
      width: 100,
      buildCell: (context, user) => Chip(
        label: Text(user.isActive ? 'Active' : 'Inactive'),
        backgroundColor: user.isActive 
          ? Colors.green.shade100 
          : Colors.grey.shade300,
        labelStyle: TextStyle(
          color: user.isActive 
            ? Colors.green.shade800 
            : Colors.grey.shade700,
        ),
      ),
    ),
    TableColumn<User>(
      key: 'lastLogin',
      label: 'Last Login',
      getValue: (user) => user.lastLogin?.toLocal().toString() ?? 'Never',
      sortable: true,
      formatter: (value) {
        if (value == 'Never') return value;
        final date = DateTime.parse(value);
        return timeago.format(date);
      },
    ),
  ],
  // Row actions
  onRowTap: (context, user) {
    context.go('/users/${user.id}');
  },
  onRowDoubleTap: (context, user) {
    context.go('/users/${user.id}/edit');
  },
  rowActions: [
    RowAction(
      icon: Icons.edit,
      tooltip: 'Edit',
      onTap: (context, user) => context.go('/users/${user.id}/edit'),
    ),
    RowAction(
      icon: Icons.delete,
      tooltip: 'Delete',
      onTap: (context, user) async {
        final confirmed = await showDeleteConfirmation(context);
        if (confirmed) {
          await UserApi().delete(user.id);
        }
      },
    ),
  ],
  // Bulk actions
  bulkActions: [
    BulkAction(
      label: 'Activate',
      icon: Icons.check_circle,
      onTap: (context, users) async {
        for (final user in users) {
          await UserApi().update(
            user.id,
            user.copyWith(isActive: true),
          );
        }
      },
    ),
    BulkAction(
      label: 'Export',
      icon: Icons.download,
      onTap: (context, users) async {
        await exportUsers(users);
      },
    ),
  ],
  // Filtering
  filters: [
    Filter<User>(
      key: 'status',
      label: 'Status',
      options: [
        FilterOption(value: 'active', label: 'Active'),
        FilterOption(value: 'inactive', label: 'Inactive'),
      ],
      apply: (users, value) {
        if (value == 'active') {
          return users.where((u) => u.isActive).toList();
        } else if (value == 'inactive') {
          return users.where((u) => !u.isActive).toList();
        }
        return users;
      },
    ),
    Filter<User>(
      key: 'role',
      label: 'Role',
      options: [
        FilterOption(value: 'admin', label: 'Admin'),
        FilterOption(value: 'manager', label: 'Manager'),
        FilterOption(value: 'user', label: 'User'),
      ],
      apply: (users, value) {
        return users.where((u) => u.role == value).toList();
      },
    ),
  ],
  // Grouping
  groupBy: GroupByConfig<User>(
    options: [
      GroupByOption(
        key: 'department',
        label: 'Department',
        getGroupKey: (user) => user.department,
      ),
      GroupByOption(
        key: 'location',
        label: 'Location',
        getGroupKey: (user) => user.location,
      ),
    ],
  ),
  // Empty state
  emptyStateBuilder: (context) => EmptyState(
    icon: Icons.people_outline,
    title: 'No users found',
    subtitle: 'Start by adding your first user',
    action: ElevatedButton.icon(
      onPressed: () => context.go('/users/new'),
      icon: const Icon(Icons.add),
      label: const Text('Add User'),
    ),
  ),
  // Advanced options
  showRowNumbers: true,
  alternateRowColors: true,
  stickyHeader: true,
  resizableColumns: true,
  reorderableColumns: true,
  exportable: true,
  defaultSort: SortConfig(column: 'name', ascending: true),
  pageSize: 25,
  pageSizeOptions: [10, 25, 50, 100],
)

GridListLayout

Card-based layout for visual entities:

GridListLayout<Equipment>(
  title: 'Equipment',
  crossAxisCount: 3,
  aspectRatio: 1.2,
  spacing: 16,
  buildCard: (context, equipment) => EquipmentCard(
    equipment: equipment,
    onTap: () => context.go('/equipment/${equipment.id}'),
    showActions: true,
  ),
  // Responsive grid
  responsive: ResponsiveGridConfig(
    mobile: 1,
    tablet: 2,
    desktop: 3,
    widescreen: 4,
  ),
  // Card actions
  cardActions: [
    CardAction(
      icon: Icons.qr_code,
      tooltip: 'Show QR Code',
      onTap: (context, equipment) => showQrCode(context, equipment),
    ),
  ],
  // Loading state
  loadingBuilder: (context) => GridView.builder(
    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 3,
      mainAxisSpacing: 16,
      crossAxisSpacing: 16,
    ),
    itemCount: 6,
    itemBuilder: (context, index) => const ShimmerCard(),
  ),
)

Custom List Layouts

Create specialized layouts for specific needs:

class TimelineListLayout<T extends EntityBase> extends EntityLayout<T> {
  final String Function(T) getTimestamp;
  final Widget Function(BuildContext, T) buildContent;
  final String Function(T)? getTitle;
  final IconData Function(T)? getIcon;
  
  @override
  Widget build(BuildContext context, EntityListViewState<T> state) {
    final groupedByDate = groupBy(
      state.filteredEntities,
      (entity) => DateFormat('yyyy-MM-dd').format(
        DateTime.parse(getTimestamp(entity)),
      ),
    );
    
    return ListView.builder(
      itemCount: groupedByDate.length,
      itemBuilder: (context, index) {
        final date = groupedByDate.keys.elementAt(index);
        final entities = groupedByDate[date]!;
        
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Date header
            Padding(
              padding: const EdgeInsets.all(16),
              child: Text(
                _formatDate(date),
                style: Theme.of(context).textTheme.titleMedium,
              ),
            ),
            // Timeline items
            ...entities.map((entity) => TimelineItem(
              timestamp: getTimestamp(entity),
              title: getTitle?.call(entity),
              icon: getIcon?.call(entity),
              content: buildContent(context, entity),
              onTap: () => _handleEntityTap(context, entity),
            )),
          ],
        );
      },
    );
  }
}

Detail View Layouts

Standard Detail Layout

class StandardDetailLayout<T extends EntityBase> extends EntityLayout<T> {
  final List<DetailSection<T>> sections;
  final Widget Function(BuildContext, T)? headerBuilder;
  final List<EntityAction<T>>? actions;
  
  @override
  Widget build(BuildContext context, T entity) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // Header
          if (headerBuilder != null)
            SliverToBoxAdapter(
              child: headerBuilder!(context, entity),
            ),
          
          // Action bar
          if (actions != null && actions!.isNotEmpty)
            SliverToBoxAdapter(
              child: ActionBar(
                actions: actions!,
                entity: entity,
              ),
            ),
          
          // Content sections
          ...sections.map((section) => SliverToBoxAdapter(
            child: DetailSectionWidget(
              section: section,
              entity: entity,
            ),
          )),
        ],
      ),
    );
  }
}

// Usage
StandardDetailLayout<User>(
  headerBuilder: (context, user) => UserHeader(user: user),
  sections: [
    DetailSection(
      title: 'Personal Information',
      icon: Icons.person,
      fields: [
        DetailField(
          label: 'Full Name',
          getValue: (user) => user.name,
        ),
        DetailField(
          label: 'Email',
          getValue: (user) => user.email,
          copyable: true,
        ),
        DetailField(
          label: 'Phone',
          getValue: (user) => user.phone ?? 'Not provided',
          action: DetailFieldAction(
            icon: Icons.phone,
            onTap: (context, value) => launchPhone(value),
          ),
        ),
      ],
    ),
    DetailSection(
      title: 'Account Information',
      icon: Icons.security,
      collapsible: true,
      initiallyExpanded: false,
      fields: [
        DetailField(
          label: 'Role',
          getValue: (user) => user.role.toUpperCase(),
          builder: (context, value) => Chip(
            label: Text(value),
            backgroundColor: _getRoleColor(value),
          ),
        ),
        DetailField(
          label: 'Status',
          getValue: (user) => user.isActive ? 'Active' : 'Inactive',
          builder: (context, value) => Row(
            children: [
              Icon(
                value == 'Active' ? Icons.check_circle : Icons.cancel,
                color: value == 'Active' ? Colors.green : Colors.red,
                size: 16,
              ),
              const SizedBox(width: 4),
              Text(value),
            ],
          ),
        ),
      ],
    ),
  ],
  actions: [
    EntityAction(
      label: 'Edit',
      icon: Icons.edit,
      onTap: (context, [user]) => context.go('/users/${user.id}/edit'),
    ),
    EntityAction(
      label: 'Reset Password',
      icon: Icons.lock_reset,
      onTap: (context, [user]) => resetPassword(user),
    ),
  ],
)

Tabbed Detail Layout

class TabbedDetailLayout<T extends EntityBase> extends EntityLayout<T> {
  final List<DetailTab<T>> tabs;
  final Widget Function(BuildContext, T)? headerBuilder;
  
  @override
  Widget build(BuildContext context, T entity) {
    return DefaultTabController(
      length: tabs.length,
      child: Scaffold(
        appBar: AppBar(
          title: Text(_getTitle(entity)),
          bottom: TabBar(
            tabs: tabs.map((tab) => Tab(
              text: tab.label,
              icon: tab.icon != null ? Icon(tab.icon) : null,
            )).toList(),
          ),
        ),
        body: Column(
          children: [
            if (headerBuilder != null) headerBuilder!(context, entity),
            Expanded(
              child: TabBarView(
                children: tabs.map((tab) => tab.builder(context, entity)).toList(),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// Usage
TabbedDetailLayout<Location>(
  tabs: [
    DetailTab(
      label: 'Overview',
      icon: Icons.info,
      builder: (context, location) => LocationOverview(location: location),
    ),
    DetailTab(
      label: 'Equipment',
      icon: Icons.devices,
      builder: (context, location) => LocationEquipmentList(
        locationId: location.id,
      ),
    ),
    DetailTab(
      label: 'Users',
      icon: Icons.people,
      builder: (context, location) => LocationUsersList(
        locationId: location.id,
      ),
    ),
    DetailTab(
      label: 'Activity',
      icon: Icons.timeline,
      builder: (context, location) => ActivityTimeline(
        entityType: 'locations',
        entityId: location.id,
      ),
    ),
  ],
)

Dashboard Layouts

Dashboard layouts contribute widgets to the main dashboard:

class EntityDashboardWidget<T extends EntityBase> extends EntityLayout<T> {
  final String title;
  final IconData icon;
  final int gridWidth;
  final int gridHeight;
  
  @override
  Widget build(BuildContext context, DashboardState state) {
    return DashboardCard(
      title: title,
      icon: icon,
      gridWidth: gridWidth,
      gridHeight: gridHeight,
      content: _buildContent(context, state),
    );
  }
}

// Statistics widget
class UserStatsWidget extends EntityDashboardWidget<User> {
  UserStatsWidget() : super(
    title: 'User Statistics',
    icon: Icons.people,
    gridWidth: 2,
    gridHeight: 1,
  );
  
  @override
  Widget _buildContent(BuildContext context, DashboardState state) {
    return FutureBuilder<UserStats>(
      future: UserApi().getStatistics(),
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return const LoadingIndicator();
        }
        
        final stats = snapshot.data!;
        return Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            _StatItem(
              label: 'Total Users',
              value: stats.total.toString(),
              icon: Icons.people,
              color: Colors.blue,
            ),
            _StatItem(
              label: 'Active',
              value: stats.active.toString(),
              icon: Icons.check_circle,
              color: Colors.green,
            ),
            _StatItem(
              label: 'New This Month',
              value: stats.newThisMonth.toString(),
              icon: Icons.trending_up,
              color: Colors.orange,
            ),
          ],
        );
      },
    );
  }
}

// Chart widget
class EquipmentUtilizationChart extends EntityDashboardWidget<Equipment> {
  EquipmentUtilizationChart() : super(
    title: 'Equipment Utilization',
    icon: Icons.bar_chart,
    gridWidth: 3,
    gridHeight: 2,
  );
  
  @override
  Widget _buildContent(BuildContext context, DashboardState state) {
    return FutureBuilder<List<UtilizationData>>(
      future: EquipmentApi().getUtilizationData(),
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return const LoadingIndicator();
        }
        
        return UtilizationChart(
          data: snapshot.data!,
          interactive: true,
          showLegend: true,
        );
      },
    );
  }
}

Layout Selection

The system automatically selects appropriate layouts based on context:

class LayoutSelector<T extends EntityBase> {
  final List<EntityLayout<T>> layouts;
  
  EntityLayout<T> selectLayout(BuildContext context) {
    // Check device type
    final isDesktop = MediaQuery.of(context).size.width > 1200;
    final isTablet = MediaQuery.of(context).size.width > 600;
    
    // Check user preferences
    final preferences = context.read<UserPreferences>();
    final preferredLayout = preferences.getLayoutPreference(T.toString());
    
    // Find matching layout
    for (final layout in layouts) {
      if (layout.canHandle(context)) {
        if (preferredLayout != null && 
            layout.runtimeType.toString() == preferredLayout) {
          return layout;
        }
        
        if (isDesktop && layout is TableListLayout) {
          return layout;
        }
        
        if (!isDesktop && layout is GridListLayout) {
          return layout;
        }
      }
    }
    
    // Return first available layout
    return layouts.first;
  }
}

Custom Layout Implementation

Create completely custom layouts:

class KanbanBoardLayout<T extends EntityBase> extends EntityLayout<T> {
  final List<KanbanColumn<T>> columns;
  final String Function(T) getColumnKey;
  final bool allowDragDrop;
  
  @override
  Widget build(BuildContext context, EntityListViewState<T> state) {
    final groupedEntities = groupBy(
      state.filteredEntities,
      getColumnKey,
    );
    
    return KanbanBoard(
      columns: columns.map((column) => KanbanColumnWidget(
        column: column,
        entities: groupedEntities[column.key] ?? [],
        onDrop: allowDragDrop ? (entity, newColumn) {
          _handleDrop(context, entity, newColumn);
        } : null,
      )).toList(),
    );
  }
  
  void _handleDrop(BuildContext context, T entity, String newColumn) {
    // Update entity with new column value
    final updated = _updateEntityColumn(entity, newColumn);
    context.read<EntityProvider<T>>().update(entity.id, updated);
  }
}

// Usage for task management
KanbanBoardLayout<Task>(
  columns: [
    KanbanColumn(key: 'todo', label: 'To Do', color: Colors.grey),
    KanbanColumn(key: 'in_progress', label: 'In Progress', color: Colors.blue),
    KanbanColumn(key: 'review', label: 'Review', color: Colors.orange),
    KanbanColumn(key: 'done', label: 'Done', color: Colors.green),
  ],
  getColumnKey: (task) => task.status,
  allowDragDrop: true,
)

Responsive Layouts

Build layouts that adapt to screen size:

class ResponsiveEntityLayout<T extends EntityBase> extends EntityLayout<T> {
  final EntityLayout<T> mobile;
  final EntityLayout<T> tablet;
  final EntityLayout<T> desktop;
  
  @override
  Widget build(BuildContext context, dynamic state) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth < 600) {
          return mobile.build(context, state);
        } else if (constraints.maxWidth < 1200) {
          return tablet.build(context, state);
        } else {
          return desktop.build(context, state);
        }
      },
    );
  }
}

// Usage
ResponsiveEntityLayout<Product>(
  mobile: ProductListLayout(),      // Simple list
  tablet: ProductGridLayout(),      // 2-column grid
  desktop: ProductTableLayout(),    // Full table
)

Layout Composition

Combine multiple layouts:

class CompositeLayout<T extends EntityBase> extends EntityLayout<T> {
  final List<LayoutComponent<T>> components;
  
  @override
  Widget build(BuildContext context, EntityListViewState<T> state) {
    return Column(
      children: [
        // Summary bar
        if (components.any((c) => c is SummaryComponent))
          SummaryBar(entities: state.entities),
        
        // Main content
        Expanded(
          child: Row(
            children: [
              // Sidebar filters
              if (components.any((c) => c is FilterSidebar))
                SizedBox(
                  width: 250,
                  child: FilterSidebar(
                    onFiltersChanged: state.updateFilters,
                  ),
                ),
              
              // Main view
              Expanded(
                child: _buildMainView(context, state),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

Performance Optimization

Virtual Scrolling

For large datasets:

class VirtualizedTableLayout<T extends EntityBase> extends TableListLayout<T> {
  @override
  Widget buildTable(BuildContext context, List<T> entities) {
    return VirtualizedDataTable(
      rowCount: entities.length,
      rowHeight: 48,
      columns: columns,
      rowBuilder: (context, index) {
        final entity = entities[index];
        return DataRow(
          cells: columns.map((column) => DataCell(
            column.buildCell?.call(context, entity) ??
            Text(column.getValue(entity)),
          )).toList(),
        );
      },
    );
  }
}

Lazy Loading

Load data as needed:

class LazyLoadLayout<T extends EntityBase> extends EntityLayout<T> {
  @override
  Widget build(BuildContext context, EntityListViewState<T> state) {
    return LazyLoadScrollView(
      onEndOfPage: () => state.loadMore(),
      child: ListView.builder(
        itemCount: state.entities.length + (state.hasMore ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == state.entities.length) {
            return const LoadingIndicator();
          }
          return EntityListItem(entity: state.entities[index]);
        },
      ),
    );
  }
}

EntityView Widget

The EntityView widget provides a unified way to handle loading, error, empty, and success states for entity data fetching and display. It uses MobX internally to manage state transitions.

Basic Usage

// With Future
EntityView<List<Equipment>>(
  future: api.getEquipment(),
  builder: (context, items) => ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) => EquipmentTile(equipment: items[index]),
  ),
)

// With Stream
EntityView<User>(
  stream: userStream,
  builder: (context, user) => UserDetailView(user: user),
)

// With Direct Data (useful with MobX observables)
Observer(
  builder: (_) => EntityView<List<Task>>(
    data: taskStore.tasks,
    builder: (context, tasks) => TaskBoard(tasks: tasks),
  ),
)

Custom States

EntityView<AnalyticsData>(
  future: fetchAnalytics(),
  title: 'Sales Analytics',
  // Custom loading widget
  loadingWidget: ShimmerLoading(
    child: AnalyticsChartSkeleton(),
  ),
  // Custom error builder
  errorBuilder: (context, error) => ErrorCard(
    title: 'Failed to load analytics',
    error: error,
    actions: [
      TextButton(
        onPressed: () => refetch(),
        child: const Text('Retry'),
      ),
    ],
  ),
  // Custom empty widget
  emptyWidget: EmptyStateIllustration(
    image: 'assets/no_data.svg',
    title: 'No analytics data',
    subtitle: 'Data will appear once sales are recorded',
  ),
  // Success state builder
  builder: (context, data) => AnalyticsChart(data: data),
)

Empty Detection

The widget automatically detects empty states for common types:

EntityView<Map<String, dynamic>>(
  future: api.getStats(),
  builder: (context, stats) => StatsGrid(stats: stats),
  // Automatically shows empty state if map is empty
)

// Custom empty detection
EntityView<CustomData>(
  future: api.getData(),
  isEmpty: (data) => data.items.isEmpty && data.total == 0,
  builder: (context, data) => CustomDataView(data: data),
)

With Entity Types

When used with entity types that extend EntityBase, the widget automatically uses typed state widgets:

EntityView<List<User>>(
  future: userApi.getAll(),
  showEntityContext: true,  // Shows entity icon and name in loading state
  showCreateButton: true,   // Shows create button in empty state
  builder: (context, users) => UserTable(users: users),
)

Retry Functionality

EntityView<ServerStatus>(
  future: checkServerStatus(),
  onRetry: () {
    // Custom retry logic
    setState(() {
      _statusFuture = checkServerStatus();
    });
  },
  showRetryButton: true,
  builder: (context, status) => ServerStatusCard(status: status),
)

Complex Example

class EquipmentDashboard extends StatefulWidget {
  @override
  State<EquipmentDashboard> createState() => _EquipmentDashboardState();
}

class _EquipmentDashboardState extends State<EquipmentDashboard> {
  late Future<DashboardData> _dashboardFuture;

  @override
  void initState() {
    super.initState();
    _dashboardFuture = _loadDashboard();
  }

  Future<DashboardData> _loadDashboard() async {
    final api = vyuh.di.get<EquipmentApi>();
    final results = await Future.wait([
      api.getStats(),
      api.getRecentActivity(),
      api.getMaintenanceSchedule(),
    ]);
    
    return DashboardData(
      stats: results[0] as EquipmentStats,
      activities: results[1] as List<Activity>,
      maintenance: results[2] as List<MaintenanceItem>,
    );
  }

  @override
  Widget build(BuildContext context) {
    return EntityView<DashboardData>(
      future: _dashboardFuture,
      title: 'Equipment Dashboard',
      loadingWidget: DashboardSkeleton(),
      errorBuilder: (context, error) => DashboardError(
        error: error,
        onRetry: () {
          setState(() {
            _dashboardFuture = _loadDashboard();
          });
        },
      ),
      isEmpty: (data) => data.stats.totalEquipment == 0,
      emptyWidget: EmptyDashboard(
        onGetStarted: () => context.go('/equipment/new'),
      ),
      builder: (context, data) => Column(
        children: [
          StatsRow(stats: data.stats),
          const SizedBox(height: 24),
          Expanded(
            child: Row(
              children: [
                Expanded(
                  flex: 2,
                  child: ActivityFeed(activities: data.activities),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: MaintenanceCalendar(items: data.maintenance),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Migration Guide

To migrate existing FutureBuilder/manual state management to EntityView:

Before:

FutureBuilder<List<Equipment>>(
  future: _loadEquipment(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(child: CircularProgressIndicator());
    }
    
    if (snapshot.hasError) {
      return Center(
        child: Text('Error: ${snapshot.error}'),
      );
    }
    
    final equipment = snapshot.data ?? [];
    if (equipment.isEmpty) {
      return const Center(
        child: Text('No equipment found'),
      );
    }
    
    return EquipmentList(equipment: equipment);
  },
)

After:

EntityView<List<Equipment>>(
  future: _loadEquipment(),
  builder: (context, equipment) => EquipmentList(equipment: equipment),
)

Manual State Management Before:

class _EquipmentViewState extends State<EquipmentView> {
  final _equipment = ObservableList<Equipment>();
  final _isLoading = Observable(true);
  final _error = Observable<String?>(null);

  @override
  void initState() {
    super.initState();
    _loadEquipment();
  }

  Future<void> _loadEquipment() async {
    try {
      runInAction(() {
        _isLoading.value = true;
        _error.value = null;
      });

      final data = await api.getEquipment();
      
      runInAction(() {
        _equipment.clear();
        _equipment.addAll(data);
        _isLoading.value = false;
      });
    } catch (e) {
      runInAction(() {
        _error.value = e.toString();
        _isLoading.value = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Observer(
      builder: (_) {
        if (_isLoading.value) {
          return const LoadingState();
        }

        if (_error.value != null) {
          return ErrorState(error: _error.value);
        }

        if (_equipment.isEmpty) {
          return const EmptyState();
        }

        return EquipmentList(equipment: _equipment);
      },
    );
  }
}

After:

class EquipmentView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return EntityView<List<Equipment>>(
      future: api.getEquipment(),
      builder: (context, equipment) => EquipmentList(equipment: equipment),
    );
  }
}

Best Practices

  1. Multiple Layouts - Provide different layouts for different use cases
  2. Responsive Design - Ensure layouts work on all screen sizes
  3. Performance - Use virtualization for large datasets
  4. Accessibility - Include proper labels and navigation
  5. Consistency - Follow platform design guidelines
  6. Customization - Allow users to choose preferred layouts
  7. Empty States - Always handle empty data gracefully
  8. Use EntityView - Prefer EntityView over manual state management for consistency

Next: Forms and Validation - Complete guide to form management