Skip to content

Data Binding

This guide explains how to connect dashboard components to live data from API endpoints, static sources, and entity queries. You will learn about field mapping, nested JSON traversal, the DashboardDataStore for polling, and best practices for reactive data display.

Data Flow Overview

Connecting to API Endpoints

The ApiChartDataSource is the primary way to fetch data from HTTP endpoints:

dart
final dataSource = ApiChartDataSource();

final data = await dataSource.fetchData(
  mapping: ChartFieldMapping(
    labelField: 'courseName',
    valueField: 'enrollmentCount',
  ),
  filters: {
    'endpoint': 'https://api.lms.example.com/enrollments',
    'dataArrayPath': 'data.enrollments',
  },
);

Field Mapping

The ChartFieldMapping tells the data source which fields in the API response map to which chart dimensions:

Mapping FieldMaps ToExample
labelFieldDataPoint.label'courseName' -- displayed on x-axis or as pie slice label
valueFieldDataPoint.value'enrollmentCount' -- the numeric value
seriesFieldDataPoint.series'level' -- groups data into multiple series

Smart Defaults

If the field mapping does not match, the data source falls back to common field names:

  • Label: tries the specified field, then key, name, label, then Item N
  • Value: tries the specified field, then value, count, then 0

Nested JSON Traversal

Real APIs often return nested JSON. The data source supports three traversal patterns:

Dot Notation

Access nested objects with dot-separated paths:

json
{
  "response": {
    "data": {
      "enrollments": [
        {"name": "Dart Basics", "count": 120}
      ]
    }
  }
}
dart
filters: {
  'endpoint': url,
  'dataArrayPath': 'response.data.enrollments',
}

Array Flattening

Flatten nested arrays with the [] operator:

json
{
  "courses": [
    {
      "level": "beginner",
      "modules": [
        {"name": "Introduction", "students": 45},
        {"name": "Basics", "students": 38}
      ]
    }
  ]
}
dart
// Flatten courses[].modules into a flat list
// Each item merges parent fields into child
filters: {
  'endpoint': url,
  'dataArrayPath': 'courses[].modules',
}

mapping: ChartFieldMapping(
  labelField: 'name',       // From child (module)
  valueField: 'students',   // From child (module)
  seriesField: 'level',     // From parent (course)
),

After flattening, each item contains both parent and child fields merged together.

Nested Field Names

Field names in the mapping also support dot notation:

dart
mapping: ChartFieldMapping(
  labelField: 'course.name',       // Nested in each item
  valueField: 'stats.enrollment',  // Nested numeric field
),

Auto-Detection

When no dataArrayPath is specified, the data source tries:

  1. If the root is an array, use it directly
  2. Look for common keys: data, items, results
  3. Use the first array value found in the root object

Static Data Sources

For dashboards that do not need API calls, provide static data directly. A store always needs a ComponentDataFormat, so define the response shape your component expects:

dart
class SummaryDataFormat implements ComponentDataFormat {
  @override
  String get displayName => 'Summary';

  @override
  String get description => 'Dashboard summary counts';

  @override
  Map<String, dynamic> getExpectedFormat() => {
    'data': {'type': 'object', 'required': true},
  };

  @override
  ValidationResult validate(dynamic data) {
    return DataFormatValidator.validate(data, this);
  }
}

final summaryDataFormat = SummaryDataFormat();

final store = DashboardDataStore(
  storeKey: 'course-distribution',
  apiDataSource: ApiDataSource(baseUrl: ''),
  dataFormat: summaryDataFormat,
);

await store.fetchStatic({
  'data': {'total': 1247, 'active': 42},
});

DashboardDataStore for Polling

The DashboardDataStore is a MobX store that wraps data fetching with observable state, validation, and polling support.

Creating a Store

dart
final store = DashboardDataStore(
  storeKey: 'enrollment-stats',       // Unique identifier
  apiDataSource: ApiDataSource(
    baseUrl: 'https://api.lms.example.com',
  ),
  dataFormat: summaryDataFormat, // Expected response format
);

Fetching Data

dart
// From API
await store.fetchFromApi('/api/enrollments/summary');

// With custom method
await store.fetchFromApi('/api/enrollments', method: 'POST');

// Static data
await store.fetchStatic({'total': 1247, 'active': 42});

Observable State

The store exposes five observable properties that your UI can react to:

dart
Observer(
  builder: (_) {
    // Loading state
    if (store.isLoading) {
      return const CircularProgressIndicator();
    }

    // Error state
    if (store.error != null) {
      return Text('Error: ${store.error}');
    }

    // Data available
    final data = store.data;
    final lastFetch = store.lastFetch;

    return Column(
      children: [
        Text('Last updated: $lastFetch'),
        buildDashboardContent(data),
      ],
    );
  },
)

Setting Up Polling

dart
// Start polling every 30 seconds
store.startPolling(
  sourceType: 'api',
  sourceData: {
    'endpoint': '/api/enrollments/summary',
    'method': 'GET',
  },
  intervalSeconds: 30,
);

// Stop polling
store.stopPolling();

// Manual refresh (one-time)
await store.refresh(
  sourceType: 'api',
  sourceData: {'endpoint': '/api/enrollments/summary'},
);

Polling Behavior

  • Polling waits the full interval before the first background fetch
  • If a fetch fails, polling continues at the next interval
  • Calling startPolling when already polling is a no-op
  • stopPolling gracefully terminates the polling loop
  • clear() stops polling and resets all state

Data Binding in Components

Here is how to integrate data binding into a ComponentDescriptor render callback:

Pattern: Inline Fetch

For simple components, fetch data directly in the render widget:

dart
final enrollmentStat = ComponentDescriptor(
  schemaType: 'lms.enrollment_stat',
  componentKey: 'lms_enrollment_stat',
  displayName: 'Enrollment Stat',
  properties: () => (PropertyCollectionBuilder()
    ..string('endpoint', 'API Endpoint',
      defaultValue: '/api/enrollments/count',
    )
    ..string('title', 'Title', defaultValue: 'Total Enrolled')
  ).buildCollection(),
  render: (context, props) {
    final endpoint = props.stringValue('endpoint') ?? '';
    final title = props.stringValue('title') ?? '';

    return FutureBuilder(
      future: ApiDataSource(baseUrl: 'https://api.lms.example.com')
          .fetch(endpoint: endpoint),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }
        if (snapshot.hasError) {
          return Center(child: Text('Error loading data'));
        }
        final value = snapshot.data?['count'] ?? 0;
        return StatCardView(title: title, value: value);
      },
    );
  },
);

Pattern: Store-Based with Polling

For components that need auto-refresh, use DashboardDataStore:

dart
class TrendDataFormat implements ComponentDataFormat {
  @override
  String get displayName => 'Trend';

  @override
  String get description => 'Trend response containing a data object';

  @override
  Map<String, dynamic> getExpectedFormat() => {
    'data': {'type': 'object', 'required': true},
  };

  @override
  ValidationResult validate(dynamic data) {
    return DataFormatValidator.validate(data, this);
  }
}

final trendDataFormat = TrendDataFormat();

class PollingChartView extends StatefulWidget {
  final PropertyCollection properties;
  const PollingChartView({super.key, required this.properties});

  @override
  State<PollingChartView> createState() => _PollingChartViewState();
}

class _PollingChartViewState extends State<PollingChartView> {
  late final DashboardDataStore _store;

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

    _store = DashboardDataStore(
      storeKey: 'chart-${widget.properties.stringValue('endpoint')}',
      apiDataSource: ApiDataSource(baseUrl: 'https://api.lms.example.com'),
      dataFormat: trendDataFormat,
    );

    final endpoint = widget.properties.stringValue('endpoint') ?? '';
    _store.fetchFromApi(endpoint);

    final enablePolling = widget.properties.boolValue('enablePolling') ?? false;
    if (enablePolling) {
      final interval = widget.properties.intValue('pollingInterval') ?? 30;
      _store.startPolling(
        sourceType: 'api',
        sourceData: {'endpoint': endpoint},
        intervalSeconds: interval,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Observer(
      builder: (_) {
        if (_store.isLoading && _store.data == null) {
          return const Center(child: CircularProgressIndicator());
        }
        if (_store.error != null && _store.data == null) {
          return Center(child: Text(_store.error!));
        }
        return buildChart(context, _store.data);
      },
    );
  }

  @override
  void dispose() {
    _store.clear();
    super.dispose();
  }
}

LMS Example: Connecting to Enrollment API

API Response

json
{
  "data": {
    "summary": {
      "totalStudents": 1247,
      "activeCourses": 42,
      "completionRate": 87.5,
      "averageScore": 78.2
    },
    "enrollmentsByMonth": [
      {"month": "Jan", "count": 120, "level": "beginner"},
      {"month": "Jan", "count": 85, "level": "intermediate"},
      {"month": "Jan", "count": 45, "level": "advanced"},
      {"month": "Feb", "count": 140, "level": "beginner"}
    ]
  }
}

Connecting Stat Cards

dart
// Fetch summary and distribute to stat cards
final store = DashboardDataStore(
  storeKey: 'lms-summary',
  apiDataSource: ApiDataSource(baseUrl: 'https://api.lms.example.com'),
  dataFormat: summaryDataFormat,
);

await store.fetchFromApi('/api/dashboard/summary');

// In render callback, access nested fields
final summary = (store.data as Map)['data']['summary'];
final totalStudents = summary['totalStudents'] as int;

Connecting the Enrollment Chart

dart
final data = await ApiChartDataSource().fetchData(
  mapping: ChartFieldMapping(
    labelField: 'month',
    valueField: 'count',
    seriesField: 'level',
  ),
  filters: {
    'endpoint': 'https://api.lms.example.com/api/dashboard/summary',
    'dataArrayPath': 'data.enrollmentsByMonth',
  },
);

// data now contains DataPoint instances ready for chart rendering
final chartData = ChartData(type: ChartType.multiLine, data: data);

Next Steps