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
| Row | Components | Flex Layout |
|---|---|---|
| Row 1: Key Metrics | 4 stat cards (Students, Courses, Completion Rate, Avg Score) | 2 + 1 + 1 + 2 = 6 |
| Row 2: Charts | Enrollment trend (line) + Course distribution (pie) | 4 + 2 = 6 |
| Row 3: Detail | Recent completions table | 6 (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
- Custom Chart Component Example -- Build a chart with entity integration
- Custom Components Guide -- Detailed component-building guide
- API Reference -- Complete API documentation