Skip to content

LMS Dashboard Example

A complete Learning Management System analytics dashboard with three rows: stat cards, charts, and a recent completions table. This example demonstrates end-to-end usage of the dashboard editor, from component definition to data binding.

Dashboard Overview

RowComponentsFlex Layout
Row 1: Key Metrics4 stat cards (Students, Courses, Completion Rate, Avg Score)2 + 1 + 1 + 2 = 6
Row 2: ChartsEnrollment trend (line) + Course distribution (pie)4 + 2 = 6
Row 3: DetailRecent completions table6 (full width)

Step 1: Category and Constants

dart
import 'package:flutter/material.dart';
import 'package:vyuh_dashboard_editor/vyuh_dashboard_editor.dart';
import 'package:vyuh_property_system/vyuh_property_system.dart';

const lmsCategory = ComponentCategory(
  id: 'lms',
  displayName: 'LMS',
  icon: Icons.school,
  sortOrder: 10,
);

const lmsApiBase = 'https://api.lms.example.com';

Step 2: Stat Card Component

dart
PropertyCollection buildStatCardProperties() {
  return (PropertyCollectionBuilder()
    ..group('content', 'Content')
    ..string('title', 'Title', required: true, defaultValue: 'Metric')
    ..integer('value', 'Value', defaultValue: 0)
    ..string('unit', 'Unit', defaultValue: '')
    ..string('trend', 'Trend', defaultValue: '+0%')
    ..enumeration('trendDirection', 'Trend Direction',
      values: ['up', 'down', 'neutral'],
      defaultValue: 'up',
    )
    ..group('appearance', 'Appearance')
    ..string('iconName', 'Icon', defaultValue: 'analytics')
    ..string('color', 'Accent Color', defaultValue: '#2196F3')
  ).buildCollection();
}

final lmsStatCard = ComponentDescriptor(
  schemaType: 'lms.stat_card',
  componentKey: 'lms_stat_card',
  displayName: 'LMS Stat Card',
  description: 'Single enrollment metric with trend indicator',
  icon: Icons.analytics,
  category: lmsCategory,
  minCellHeight: 100,
  properties: buildStatCardProperties,
  render: (context, props) {
    final theme = Theme.of(context);
    final title = props.stringValue('title') ?? 'Metric';
    final value = props.intValue('value') ?? 0;
    final unit = props.stringValue('unit') ?? '';
    final trend = props.stringValue('trend') ?? '';
    final direction = props.stringValue('trendDirection') ?? 'up';

    final trendColor = switch (direction) {
      'up' => Colors.green,
      'down' => Colors.red,
      _ => theme.colorScheme.onSurfaceVariant,
    };

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(title, style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            )),
            const SizedBox(height: 8),
            Row(
              crossAxisAlignment: CrossAxisAlignment.baseline,
              textBaseline: TextBaseline.alphabetic,
              children: [
                Text('$value', style: theme.textTheme.headlineMedium?.copyWith(
                  fontWeight: FontWeight.bold,
                )),
                if (unit.isNotEmpty) ...[
                  const SizedBox(width: 4),
                  Text(unit, style: theme.textTheme.titleSmall),
                ],
              ],
            ),
            if (trend.isNotEmpty) ...[
              const SizedBox(height: 4),
              Text(trend, style: theme.textTheme.bodySmall?.copyWith(
                color: trendColor,
                fontWeight: FontWeight.w600,
              )),
            ],
          ],
        ),
      ),
    );
  },
);

Step 3: Enrollment Trend Chart Component

dart
PropertyCollection buildTrendChartProperties() {
  return (PropertyCollectionBuilder()
    ..group('data', 'Data Source')
    ..string('endpoint', 'API Endpoint',
      defaultValue: '/api/enrollments/monthly',
    )
    ..string('dataArrayPath', 'Data Path', defaultValue: 'data')
    ..string('labelField', 'Label Field', defaultValue: 'month')
    ..string('valueField', 'Value Field', defaultValue: 'count')
    ..string('seriesField', 'Series Field', defaultValue: 'level')
    ..group('appearance', 'Appearance')
    ..string('title', 'Chart Title', defaultValue: 'Enrollment Trends')
    ..boolean('showLegend', 'Show Legend', defaultValue: true)
  ).buildCollection();
}

final lmsEnrollmentTrend = ComponentDescriptor(
  schemaType: 'lms.enrollment_trend',
  componentKey: 'lms_enrollment_trend',
  displayName: 'Enrollment Trend',
  description: 'Multi-line chart of enrollments over time by level',
  icon: Icons.show_chart,
  category: lmsCategory,
  defaultSize: const Size(4, 3),
  minCellHeight: 250,
  properties: buildTrendChartProperties,
  render: (context, props) {
    return _EnrollmentTrendChart(properties: props);
  },
);

class _EnrollmentTrendChart extends StatefulWidget {
  final PropertyCollection properties;
  const _EnrollmentTrendChart({required this.properties});

  @override
  State<_EnrollmentTrendChart> createState() => _EnrollmentTrendChartState();
}

class _EnrollmentTrendChartState extends State<_EnrollmentTrendChart> {
  List<DataPoint>? _data;
  bool _loading = true;
  String? _error;

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

  Future<void> _fetchData() async {
    try {
      final props = widget.properties;
      final endpoint = '$lmsApiBase${props.stringValue('endpoint') ?? ''}';

      final data = await ApiChartDataSource().fetchData(
        mapping: ChartFieldMapping(
          labelField: props.stringValue('labelField') ?? 'month',
          valueField: props.stringValue('valueField') ?? 'count',
          seriesField: props.stringValue('seriesField'),
        ),
        filters: {
          'endpoint': endpoint,
          'dataArrayPath': props.stringValue('dataArrayPath') ?? 'data',
        },
      );

      if (mounted) setState(() { _data = data; _loading = false; });
    } catch (e) {
      if (mounted) setState(() { _error = '$e'; _loading = false; });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) return const Center(child: CircularProgressIndicator());
    if (_error != null) return Center(child: Text('Error: $_error'));

    final chartData = ChartData(type: ChartType.multiLine, data: _data!);
    final renderer = ChartRendererFactory().createRenderer(chartData);
    final title = widget.properties.stringValue('title') ?? '';
    final showLegend = widget.properties.boolValue('showLegend') ?? true;

    return renderer.build(
      context,
      chartData,
      title: title.isNotEmpty ? title : null,
      showLegend: showLegend,
      seriesColors: {
        'beginner': const Color(0xFF4CAF50),
        'intermediate': const Color(0xFFFF9800),
        'advanced': const Color(0xFF9C27B0),
      },
    );
  }
}

Step 4: Course Distribution Pie Chart Component

dart
final lmsCourseDistribution = ComponentDescriptor(
  schemaType: 'lms.course_distribution',
  componentKey: 'lms_course_distribution',
  displayName: 'Course Distribution',
  description: 'Pie chart of courses by level',
  icon: Icons.pie_chart,
  category: lmsCategory,
  minCellHeight: 250,
  properties: () => (PropertyCollectionBuilder()
    ..string('title', 'Title', defaultValue: 'Course Distribution')
    ..boolean('showLegend', 'Show Legend', defaultValue: true)
    ..enumeration('chartType', 'Style',
      values: ['pie', 'donut'],
      defaultValue: 'pie',
    )
  ).buildCollection(),
  render: (context, props) {
    final typeStr = props.stringValue('chartType') ?? 'pie';
    final showLegend = props.boolValue('showLegend') ?? true;
    final title = props.stringValue('title') ?? '';

    final chartData = ChartData(
      type: ChartType.fromString(typeStr),
      data: [
        DataPoint(label: 'Beginner', value: 45),
        DataPoint(label: 'Intermediate', value: 35),
        DataPoint(label: 'Advanced', value: 20),
      ],
    );

    final renderer = ChartRendererFactory().createRenderer(chartData);
    return renderer.build(
      context,
      chartData,
      title: title.isNotEmpty ? title : null,
      showLegend: showLegend,
      seriesColors: {
        'Beginner': const Color(0xFF4CAF50),
        'Intermediate': const Color(0xFF2196F3),
        'Advanced': const Color(0xFF9C27B0),
      },
    );
  },
);

Step 5: Recent Completions Table Component

dart
final lmsCompletionsTable = ComponentDescriptor(
  schemaType: 'lms.completions_table',
  componentKey: 'lms_completions_table',
  displayName: 'Recent Completions',
  description: 'Table of recent course completions',
  icon: Icons.table_chart,
  category: lmsCategory,
  defaultSize: const Size(6, 3),
  minCellHeight: 200,
  properties: () => (PropertyCollectionBuilder()
    ..integer('limit', 'Max Rows', defaultValue: 10)
    ..string('endpoint', 'API Endpoint',
      defaultValue: '/api/completions/recent',
    )
  ).buildCollection(),
  render: (context, props) {
    final theme = Theme.of(context);

    // Static data for demonstration
    final completions = [
      {'student': 'Alice Chen', 'course': 'Dart Basics', 'date': '2026-03-25', 'score': 92},
      {'student': 'Bob Smith', 'course': 'Flutter UI', 'date': '2026-03-24', 'score': 88},
      {'student': 'Carol Jones', 'course': 'State Management', 'date': '2026-03-24', 'score': 95},
      {'student': 'Dave Wilson', 'course': 'API Integration', 'date': '2026-03-23', 'score': 79},
      {'student': 'Eve Brown', 'course': 'Dart Basics', 'date': '2026-03-23', 'score': 85},
    ];

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Recent Completions', style: theme.textTheme.titleMedium),
            const SizedBox(height: 12),
            Expanded(
              child: SingleChildScrollView(
                child: DataTable(
                  columns: const [
                    DataColumn(label: Text('Student')),
                    DataColumn(label: Text('Course')),
                    DataColumn(label: Text('Date')),
                    DataColumn(label: Text('Score'), numeric: true),
                  ],
                  rows: completions.map((c) => DataRow(cells: [
                    DataCell(Text(c['student'] as String)),
                    DataCell(Text(c['course'] as String)),
                    DataCell(Text(c['date'] as String)),
                    DataCell(Text('${c['score']}%')),
                  ])).toList(),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  },
);

Step 6: Register All Components

dart
DashboardEditorRegistry registerLmsComponents() {
  return DashboardEditorRegistry([
    DashboardDescriptor(
      name: 'LMS',
      description: 'Learning Management System dashboards',
      category: lmsCategory,
      tags: ['education', 'analytics', 'enrollment'],
      components: [
        lmsStatCard,
        lmsEnrollmentTrend,
        lmsCourseDistribution,
        lmsCompletionsTable,
      ],
    ),
  ]);
}

Step 7: Assemble the Dashboard Layout

dart
DashboardLayout buildLmsDashboard(DashboardEditorRegistry registry) {
  return DashboardLayout(
    metadata: {
      'name': 'LMS Analytics Dashboard',
      'apiBaseUrl': lmsApiBase,
    },
    rows: [
      // Row 1: Stat cards
      DashboardRow(
        id: 'row-stats',
        order: 0,
        title: 'Key Metrics',
        height: 140,
        columns: [
          DashboardColumn.withComponent('col-students',
            ComponentInstance.withDefaults(
              id: 's-students',
              componentKey: 'lms_stat_card',
              registry: registry,
              configOverrides: {
                'title': 'Total Students', 'value': 1247,
                'trend': '+12%', 'trendDirection': 'up', 'color': '#2196F3',
              },
            ), flex: 2,
          ),
          DashboardColumn.withComponent('col-courses',
            ComponentInstance.withDefaults(
              id: 's-courses',
              componentKey: 'lms_stat_card',
              registry: registry,
              configOverrides: {
                'title': 'Active Courses', 'value': 42,
                'trend': '+3', 'trendDirection': 'up', 'color': '#4CAF50',
              },
            ), flex: 1,
          ),
          DashboardColumn.withComponent('col-rate',
            ComponentInstance.withDefaults(
              id: 's-rate',
              componentKey: 'lms_stat_card',
              registry: registry,
              configOverrides: {
                'title': 'Completion Rate', 'value': 87, 'unit': '%',
                'trend': '+5%', 'trendDirection': 'up', 'color': '#FF9800',
              },
            ), flex: 1,
          ),
          DashboardColumn.withComponent('col-score',
            ComponentInstance.withDefaults(
              id: 's-score',
              componentKey: 'lms_stat_card',
              registry: registry,
              configOverrides: {
                'title': 'Avg Score', 'value': 78, 'unit': '%',
                'trend': '-2%', 'trendDirection': 'down', 'color': '#9C27B0',
              },
            ), flex: 2,
          ),
        ],
      ),

      // Row 2: Charts
      DashboardRow(
        id: 'row-charts',
        order: 1,
        title: 'Trends & Distribution',
        height: 300,
        columns: [
          DashboardColumn.withComponent('col-trend',
            ComponentInstance.withDefaults(
              id: 's-trend',
              componentKey: 'lms_enrollment_trend',
              registry: registry,
              title: 'Enrollment Trends',
            ), flex: 4,
          ),
          DashboardColumn.withComponent('col-dist',
            ComponentInstance.withDefaults(
              id: 's-dist',
              componentKey: 'lms_course_distribution',
              registry: registry,
              title: 'Course Distribution',
            ), flex: 2,
          ),
        ],
      ),

      // Row 3: Detail table
      DashboardRow(
        id: 'row-detail',
        order: 2,
        title: 'Recent Activity',
        height: 280,
        columns: [
          DashboardColumn.withComponent('col-table',
            ComponentInstance.withDefaults(
              id: 's-table',
              componentKey: 'lms_completions_table',
              registry: registry,
              title: 'Recent Completions',
            ), flex: 6,
          ),
        ],
      ),
    ],
  );
}

Step 8: Wire Up the Editor

dart
class LmsDashboardPage extends StatefulWidget {
  const LmsDashboardPage({super.key});

  @override
  State<LmsDashboardPage> createState() => _LmsDashboardPageState();
}

class _LmsDashboardPageState extends State<LmsDashboardPage> {
  late final DashboardEditorRegistry _registry;
  late final DashboardEditorController _controller;
  late final PropertyCollection _dashboardProperties;
  late final PropertyCollection _layoutSettings;
  late final ValueNotifier<bool> _previewMode;
  late final PaletteVisibilityController _paletteController;

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

    // Register components (typically done once at app startup)
    _registry = registerLmsComponents();

    // Create the controller with initial layout
    _controller = DashboardEditorController(
      initialLayout: buildLmsDashboard(_registry),
      registry: _registry,
    );
    _dashboardProperties = buildDashboardProperties();
    _layoutSettings = buildDashboardLayoutSettingsProperties();
    _previewMode = ValueNotifier(false);
    _paletteController = PaletteVisibilityController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: DashboardDesignerTab(
        controller: _controller,
        dashboardProperties: _dashboardProperties,
        layoutSettings: _layoutSettings,
        previewModeNotifier: _previewMode,
        paletteController: _paletteController,
      ),
    );
  }

  @override
  void dispose() {
    _previewMode.dispose();
    _paletteController.dispose();
    _controller.dispose();
    super.dispose();
  }
}

JSON Output

The dashboard serializes to this JSON structure for persistence:

json
{
  "version": "2.0.0",
  "rows": [
    {
      "id": "row-stats",
      "order": 0,
      "title": "Key Metrics",
      "height": 140.0,
      "columns": [
        {
          "id": "col-students",
          "flex": 2,
          "component": {
            "id": "s-students",
            "componentKey": "lms_stat_card",
            "properties": {
              "title": "Total Students",
              "value": 1247,
              "trend": "+12%",
              "trendDirection": "up",
              "color": "#2196F3"
            }
          }
        }
      ]
    }
  ],
  "metadata": {
    "name": "LMS Analytics Dashboard",
    "apiBaseUrl": "https://api.lms.example.com"
  }
}

Next Steps