Building Dashboards
This guide walks through building a complete LMS analytics dashboard step by step. You will define the layout, create rows with stat cards and charts, configure data sources, and wire up data polling.
What We Are Building
An LMS analytics dashboard with three rows:
- Stat cards row -- Total Students, Active Courses, Completion Rate, Average Score (4 columns)
- Charts row -- Enrollment trend line chart (wide) + Course distribution pie (narrow)
- Detail row -- Recent completions data table (full width)
Step 1: Define Component Descriptors
Before building the layout, you need components. Define them as ComponentDescriptor instances.
Stat Card
PropertyCollection buildStatCardProperties() {
return (PropertyCollectionBuilder()
..group('content', 'Content')
..string('title', 'Title', defaultValue: 'Metric')
..integer('value', 'Value', defaultValue: 0)
..string('unit', 'Unit', defaultValue: '')
..string('trend', 'Trend', defaultValue: '+0%')
..group('appearance', 'Appearance')
..enumeration('trendDirection', 'Direction',
values: ['up', 'down', 'neutral'],
defaultValue: 'up',
)
..string('iconName', 'Icon', defaultValue: 'people')
..string('color', 'Accent Color', defaultValue: '#2196F3')
).buildCollection();
}
final statCardDescriptor = ComponentDescriptor(
schemaType: 'lms.stat_card',
componentKey: 'lms_stat_card',
displayName: 'Stat Card',
description: 'Single metric with trend indicator',
icon: Icons.analytics,
category: lmsCategory,
minCellHeight: 100,
properties: buildStatCardProperties,
render: (context, props) => LmsStatCardView(properties: props),
);Line Chart
PropertyCollection buildLineChartProperties() {
return (PropertyCollectionBuilder()
..group('data', 'Data Source')
..string('endpoint', 'API Endpoint',
defaultValue: '/api/enrollments/monthly',
)
..string('labelField', 'Label Field', defaultValue: 'month')
..string('valueField', 'Value Field', defaultValue: 'count')
..string('seriesField', 'Series Field', defaultValue: 'level')
..group('appearance', 'Appearance')
..boolean('showLegend', 'Show Legend', defaultValue: true)
..boolean('showArea', 'Show Area Fill', defaultValue: false)
..string('seriesColors', 'Series Colors (JSON)', defaultValue: '')
).buildCollection();
}
final lineChartDescriptor = ComponentDescriptor(
schemaType: 'lms.line_chart',
componentKey: 'lms_line_chart',
displayName: 'Enrollment Trend',
icon: Icons.show_chart,
category: lmsCategory,
minCellHeight: 250,
defaultSize: const Size(4, 3),
properties: buildLineChartProperties,
render: (context, props) => LmsLineChartView(properties: props),
);Pie Chart
final pieChartDescriptor = ComponentDescriptor(
schemaType: 'lms.pie_chart',
componentKey: 'lms_pie_chart',
displayName: 'Course Distribution',
icon: Icons.pie_chart,
category: lmsCategory,
minCellHeight: 250,
properties: () => (PropertyCollectionBuilder()
..string('endpoint', 'API Endpoint',
defaultValue: '/api/courses/by-level',
)
..string('labelField', 'Label Field', defaultValue: 'level')
..string('valueField', 'Value Field', defaultValue: 'count')
..boolean('showLegend', 'Show Legend', defaultValue: true)
).buildCollection(),
render: (context, props) => LmsPieChartView(properties: props),
);Data Table
final completionsTableDescriptor = ComponentDescriptor(
schemaType: 'lms.completions_table',
componentKey: 'lms_completions_table',
displayName: 'Recent Completions',
icon: Icons.table_chart,
category: lmsCategory,
minCellHeight: 200,
defaultSize: const Size(6, 3),
properties: () => (PropertyCollectionBuilder()
..string('endpoint', 'API Endpoint',
defaultValue: '/api/completions/recent',
)
..integer('limit', 'Max Rows', defaultValue: 10)
).buildCollection(),
render: (context, props) => LmsCompletionsTableView(properties: props),
);Step 2: Register Components
Group all components into a DashboardDescriptor and register:
const lmsCategory = ComponentCategory(
id: 'lms',
displayName: 'LMS',
icon: Icons.school,
sortOrder: 10,
);
final registry = DashboardEditorRegistry([
DashboardDescriptor(
name: 'LMS',
description: 'Learning Management System dashboards',
tags: ['education', 'analytics'],
components: [
statCardDescriptor,
lineChartDescriptor,
pieChartDescriptor,
completionsTableDescriptor,
],
),
);Step 3: Define the Layout
Create the three-row layout with appropriate flex values. Remember: each row has 6 flex units total.
// Row 1: Four stat cards (flex 2+1+1+2 = 6)
final statsRow = 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',
'iconName': 'people',
'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',
'iconName': 'book',
'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',
'iconName': 'check_circle',
'color': '#FF9800',
},
),
flex: 1,
),
DashboardColumn.withComponent('col-score',
ComponentInstance.withDefaults(
id: 's-score',
componentKey: 'lms_stat_card',
registry: registry,
configOverrides: {
'title': 'Average Score',
'value': 78,
'unit': '%',
'trend': '-2%',
'trendDirection': 'down',
'iconName': 'grade',
'color': '#9C27B0',
},
),
flex: 2,
),
],
);
// Row 2: Charts (flex 4+2 = 6)
final chartsRow = DashboardRow(
id: 'row-charts',
order: 1,
title: 'Trends & Distribution',
height: 300,
columns: [
DashboardColumn.withComponent('col-trend',
ComponentInstance.withDefaults(
id: 's-trend',
componentKey: 'lms_line_chart',
registry: registry,
title: 'Enrollment Trends',
),
flex: 4,
),
DashboardColumn.withComponent('col-dist',
ComponentInstance.withDefaults(
id: 's-dist',
componentKey: 'lms_pie_chart',
registry: registry,
title: 'Course Distribution',
),
flex: 2,
),
],
);
// Row 3: Detail table (flex 6 = full width)
final detailRow = DashboardRow(
id: 'row-detail',
order: 2,
title: 'Recent Completions',
height: 250,
columns: [
DashboardColumn.withComponent('col-table',
ComponentInstance.withDefaults(
id: 's-table',
componentKey: 'lms_completions_table',
registry: registry,
),
flex: 6,
),
],
);
// Assemble the dashboard
final lmsDashboard = DashboardLayout(
metadata: {
'apiBaseUrl': 'https://api.lms.example.com',
'name': 'LMS Analytics',
'description': 'Learning Management System dashboard',
},
rows: [statsRow, chartsRow, detailRow],
);Step 4: Wire Up the Engine
Create the engine, add extensions, and wrap it in a controller:
final controller = DashboardEditorController(
initialLayout: lmsDashboard,
registry: registry,
);The controller automatically creates and wires up all four built-in extensions: history, persistence, selection, and analytics.
Step 5: Configure Data Sources
In your component render callbacks, set up data fetching:
class EnrollmentTrendDataFormat implements ComponentDataFormat {
@override
String get displayName => 'Enrollment Trend';
@override
String get description => 'Expected enrollment trend response shape';
@override
Map<String, dynamic> getExpectedFormat() => {
'data': {'type': 'array', 'required': true},
};
@override
ValidationResult validate(dynamic data) {
return DataFormatValidator.validate(data, this);
}
}
final enrollmentTrendDataFormat = EnrollmentTrendDataFormat();
class LmsLineChartView extends StatefulWidget {
final PropertyCollection properties;
const LmsLineChartView({super.key, required this.properties});
@override
State<LmsLineChartView> createState() => _LmsLineChartViewState();
}
class _LmsLineChartViewState extends State<LmsLineChartView> {
late final DashboardDataStore _store;
@override
void initState() {
super.initState();
final endpoint = widget.properties.stringValue('endpoint')
?? '/api/enrollments/monthly';
_store = DashboardDataStore(
storeKey: 'enrollment-trends',
apiDataSource: ApiDataSource(baseUrl: 'https://api.lms.example.com'),
dataFormat: enrollmentTrendDataFormat,
);
_store.fetchFromApi(endpoint);
}
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) {
if (_store.isLoading) return const Center(child: CircularProgressIndicator());
if (_store.error != null) return Center(child: Text(_store.error!));
// Transform data and render chart
final chartData = transformToChartData(_store.data);
final renderer = ChartRendererFactory().createRenderer(chartData);
return renderer.build(context, chartData, showLegend: true);
},
);
}
@override
void dispose() {
_store.clear();
super.dispose();
}
}Step 6: Set Up Polling
For real-time data, start polling in the data store:
// In initState or after initial fetch
_store.startPolling(
sourceType: 'api',
sourceData: {'endpoint': endpoint, 'method': 'GET'},
intervalSeconds: 30,
);The store automatically handles:
- Background fetching at the specified interval
- Updating the observable
dataproperty - Continuing polling even if a fetch fails
- Stopping when
stopPolling()is called or the store is cleared
Step 7: Render the Editor
Display the full dashboard editor with canvas, palette, and config panel:
final dashboardProperties = buildDashboardProperties();
final layoutSettings = buildDashboardLayoutSettingsProperties();
final previewModeNotifier = ValueNotifier(false);
final paletteController = PaletteVisibilityController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: DashboardDesignerTab(
controller: controller,
dashboardProperties: dashboardProperties,
layoutSettings: layoutSettings,
previewModeNotifier: previewModeNotifier,
paletteController: paletteController,
),
);
}Modifying the Dashboard Programmatically
After the initial layout, you can modify the dashboard through the engine API:
// Add a new row below the stat cards
final newRowId = await controller.addRow(position: 1);
// Split it into 3 columns
await controller.splitRow(newRowId, 3);
// Place a component in the first column
await controller.addComponent(
rowId: newRowId,
columnId: controller.engine.layout.rows[1].columns[0].id,
componentKey: 'lms_stat_card',
);
// Resize columns: 3+2+1 = 6
await controller.updateRowFlex(
rowId: newRowId,
flexByColumnId: {
controller.engine.layout.rows[1].columns[0].id: 3,
controller.engine.layout.rows[1].columns[1].id: 2,
controller.engine.layout.rows[1].columns[2].id: 1,
},
);
// Undo everything
await controller.undo();
await controller.undo();
await controller.undo();
await controller.undo();Next Steps
- Custom Components -- Build stat cards, KPIs, and chart components
- Data Binding -- Connect to API endpoints and entity queries
- LMS Dashboard Example -- Complete working example