Skip to content

Layout Model

The dashboard layout is a hierarchical model: DashboardLayout contains DashboardRow instances, each row contains DashboardColumn instances, and each column can hold one ComponentInstance.

Hierarchy

DashboardLayout

The top-level container for the entire dashboard. It holds an ordered list of rows plus optional metadata.

dart
class DashboardLayout {
  final String version;                  // Schema version (default: '2.0.0')
  final List<DashboardRow> rows;         // Ordered list of rows
  final Map<String, dynamic>? metadata;  // Optional metadata (name, apiBaseUrl, etc.)

  // Constants
  static const int maxRows = 25;

  // Queries
  bool get canAddRow;       // rows.length < maxRows
  int get rowCount;         // Number of rows
  bool get isEmpty;         // No rows
  int get componentCount;   // Total filled columns across all rows
  int get totalColumns;     // Total columns across all rows
  int get emptySlotCount;   // Total empty columns
  String? get apiBaseUrl;   // API base URL from metadata

  // Factories
  factory DashboardLayout.empty();
  factory DashboardLayout.withRows(List<DashboardRow> rows);

  // Mutation
  DashboardLayout copyWith({...});
  DashboardLayout withApiBaseUrl(String baseUrl);

  // Serialization
  factory DashboardLayout.fromJson(
    Map<String, dynamic> json, {
    required DashboardEditorRegistry registry,
  });
  Map<String, dynamic> toJson();
}

JSON Structure

json
{
  "version": "2.0.0",
  "rows": [
    {
      "id": "row-stats",
      "order": 0,
      "title": "Enrollment Overview",
      "height": 140,
      "columns": [
        {
          "id": "col-1",
          "flex": 2,
          "component": {
            "id": "comp-1",
            "componentKey": "lms_stat_card",
            "title": "Total Students",
            "properties": {
              "title": "Total Students",
              "value": 1247
            }
          }
        }
      ]
    }
  ],
  "metadata": {
    "apiBaseUrl": "https://api.lms.example.com"
  }
}

DashboardRow

A horizontal container for 1-6 columns. Rows stack vertically and are ordered by their order field.

dart
class DashboardRow {
  final String id;                        // Unique row ID
  final List<DashboardColumn> columns;    // 1-6 columns
  final int order;                        // Display order (0-based)
  final String? title;                    // Optional display title
  final double? height;                   // Fixed height in pixels (null = default)
  final double? viewportFraction;         // Viewport-relative height (0.1+)
  final Map<String, dynamic>? metadata;   // Optional metadata

  // Constants
  static const int maxColumns = 6;

  // Height
  double get effectiveHeight;             // height ?? 60.0
  bool get usesViewportFraction;          // viewportFraction != null

  // Column queries
  bool get canAddColumn;                  // columns.length < 6
  bool get hasEmptyColumns;
  bool get isCompletelyEmpty;
  bool get isCompletelyFilled;
  int get columnCount;
  int get totalFlex;                      // Sum of all column flex values
  List<DashboardColumn> get emptyColumns;
  List<DashboardColumn> get filledColumns;

  // Factories
  factory DashboardRow.single(String id, int order, String columnId);
  factory DashboardRow.withColumns(String id, int order, List<String> columnIds);

  // Flex distribution
  static List<int> distributeFlex(int count);  // Evenly distribute 6 flex units
  DashboardRow normalizedFlex();               // Normalize to sum to 6
}

Row Sizing

Rows support two sizing modes:

Fixed height (default): The row has a height in pixels, set via the height field. If null, the default minimum of 60 pixels is used.

Viewport fraction: When viewportFraction is set (0.1 or greater), the row height becomes a fraction of the viewport height. This is useful for full-screen dashboard rows.

dart
// Fixed height row
DashboardRow(id: 'r1', order: 0, height: 200, columns: [...]);

// Viewport-relative row (takes 50% of screen)
DashboardRow(id: 'r2', order: 1, viewportFraction: 0.5, columns: [...]);

Flex Distribution

When creating a row with multiple columns, flex values are distributed evenly. With 6 total flex units:

ColumnsFlex DistributionWidths
1[6]100%
2[3, 3]50%, 50%
3[2, 2, 2]33%, 33%, 33%
4[2, 2, 1, 1]33%, 33%, 17%, 17%
5[2, 1, 1, 1, 1]33%, 17%, 17%, 17%, 17%
6[1, 1, 1, 1, 1, 1]17% each

When the column count does not divide evenly into 6, the remainder is distributed to leading columns.

DashboardColumn

A vertical slot within a row that holds zero or one component. Columns use a flex factor for relative sizing.

dart
class DashboardColumn {
  final String id;                     // Unique column ID
  final int flex;                      // Flex factor (1-6), default: 1
  final ComponentInstance? component;   // Component (null if empty)

  // Queries
  bool get isEmpty;        // component == null
  bool get hasComponent;   // component != null

  // Factories
  factory DashboardColumn.empty(String id, {int flex = 1});
  factory DashboardColumn.withComponent(String id, ComponentInstance component, {int flex = 1});

  // Mutation
  DashboardColumn copyWith({String? id, int? flex, ComponentInstance? component, bool clearComponent});
}

Flex Sizing

Columns within a row must sum to DashboardLayoutConstants.totalFlexPerRow (6). The engine automatically normalizes flex values when columns are added or removed.

dart
// Three equal columns: 2 + 2 + 2 = 6
DashboardColumn(id: 'a', flex: 2, component: componentA),
DashboardColumn(id: 'b', flex: 2, component: componentB),
DashboardColumn(id: 'c', flex: 2, component: componentC),

// Two columns, 2:1 ratio: 4 + 2 = 6
DashboardColumn(id: 'a', flex: 4, component: chartComponent),
DashboardColumn(id: 'b', flex: 2, component: sidebarComponent),

ComponentInstance

A component instance placed inside a column. It references a registered component by key and stores its configuration as a PropertyCollection.

dart
class ComponentInstance {
  final String id;                          // Unique component ID
  final String componentKey;                // References registered component
  final String? title;                      // Optional display title
  final PropertyCollection properties;      // Type-safe component config
  final Map<String, dynamic>? metadata;     // Optional metadata

  // Factory with defaults from the scoped dashboard registry
  factory ComponentInstance.withDefaults({
    required String id,
    required String componentKey,
    required DashboardEditorRegistry registry,
    String? title,
    Map<String, dynamic>? configOverrides,
  });
}

Creating Components

The withDefaults factory looks up the component in the DashboardEditorRegistry, calls its createProperties() factory, and applies any overrides:

dart
// Component with default properties from the component descriptor
final component = ComponentInstance.withDefaults(
  id: 'comp-enrollment',
  componentKey: 'lms_stat_card',
  registry: registry,
  title: 'Total Enrolled Students',
  configOverrides: {
    'title': 'Total Enrolled',
    'value': 1247,
    'trend': '+12%',
  },
);

Component Serialization

Components serialize their PropertyCollection under the properties key. During deserialization, the component looks up its descriptor in the registry to recreate the proper PropertyCollection type:

json
{
  "id": "comp-enrollment",
  "componentKey": "lms_stat_card",
  "title": "Total Enrolled Students",
  "properties": {
    "title": "Total Enrolled",
    "value": 1247,
    "trend": "+12%"
  }
}

If the component key is not found in the registry during deserialization (for example, the component was unregistered), the component is created with an empty PropertyCollection and the renderer shows an error widget.

Layout Constants

The DashboardLayoutConstants class centralizes all sizing and spacing values:

ConstantValuePurpose
cellGap16.0Gap between rows and columns
minCellHeight60.0Default minimum row height
maxCellHeight800.0Maximum row height
totalFlexPerRow6Flex units per row
minColumnFlex1Minimum flex per column
editorInset12.0Inset padding in editor mode
toolbarHeight48.0Editor toolbar height
resizeHandleHeight8.0Height of row resize handles

LMS Example: Enrollment Dashboard Layout

Here is a complete LMS enrollment dashboard layout with three rows:

dart
final enrollmentDashboard = DashboardLayout(
  metadata: {'apiBaseUrl': 'https://api.lms.example.com'},
  rows: [
    // Row 1: KPI stat cards (3 equal columns)
    DashboardRow(
      id: 'row-kpis',
      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},
          ),
          flex: 2,
        ),
        DashboardColumn.withComponent('col-courses',
          ComponentInstance.withDefaults(
            id: 's-courses',
            componentKey: 'lms_stat_card',
            registry: registry,
            configOverrides: {'title': 'Active Courses', 'value': 42},
          ),
          flex: 2,
        ),
        DashboardColumn.withComponent('col-rate',
          ComponentInstance.withDefaults(
            id: 's-rate',
            componentKey: 'lms_stat_card',
            registry: registry,
            configOverrides: {'title': 'Completion Rate', 'value': 87},
          ),
          flex: 2,
        ),
      ],
    ),

    // Row 2: Charts (2:1 ratio)
    DashboardRow(
      id: 'row-charts',
      order: 1,
      title: 'Trends',
      height: 300,
      columns: [
        DashboardColumn.withComponent('col-trend',
          ComponentInstance.withDefaults(
            id: 's-trend',
            componentKey: 'lms_enrollment_chart',
            registry: registry,
            configOverrides: {'chartType': 'line'},
          ),
          flex: 4,
        ),
        DashboardColumn.withComponent('col-dist',
          ComponentInstance.withDefaults(
            id: 's-dist',
            componentKey: 'lms_course_distribution',
            registry: registry,
            configOverrides: {'chartType': 'pie'},
          ),
          flex: 2,
        ),
      ],
    ),

    // Row 3: Full-width detail table
    DashboardRow(
      id: 'row-details',
      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,
        ),
      ],
    ),
  ],
);

Next Steps