如何在StreamBuilder中创建列表并展示Firebase数据至Syncfusion折线图
解决Flutter中Syncfusion图表结合Firebase流展示传感器数据的问题及替代方案
一、修复Syncfusion折线图X轴无数据的问题
1. 定义传感器数据模型
先创建数据类解析Firebase返回的JSON数据:
class SensorData { final double ec; final double ph; final double rtd; final double humidity; final double temperature; final DateTime timestamp; SensorData({ required this.ec, required this.ph, required this.rtd, required this.humidity, required this.temperature, required this.timestamp, }); // 从Firebase Map数据转模型 factory SensorData.fromMap(Map<String, dynamic> map) { return SensorData( ec: (map['EC'] as num).toDouble(), ph: (map['PH'] as num).toDouble(), rtd: (map['RTD'] as num).toDouble(), humidity: (map['湿度'] as num).toDouble(), temperature: (map['温度'] as num).toDouble(), timestamp: DateTime.fromMillisecondsSinceEpoch(map['timestamp']), ); } }
2. 转换Firebase单条数据流为列表流
onChildAdded每次只返回单条新增数据,需要将数据累加为完整列表流:
final List<SensorData> _sensorDataList = []; // 转换流为累加后的列表流 Stream<List<SensorData>> get sensorDataStream { return FirebaseDatabase.instance.ref('sensor_data').onChildAdded.map((event) { final data = SensorData.fromMap(event.snapshot.value as Map<String, dynamic>); _sensorDataList.add(data); // 按时间戳排序,保证数据顺序正确 _sensorDataList.sort((a, b) => a.timestamp.compareTo(b.timestamp)); return List.from(_sensorDataList); }); }
3. 正确配置Syncfusion Cartesian Chart
在StreamBuilder中配置图表,重点处理X轴的时间映射:
StreamBuilder<List<SensorData>>( stream: sensorDataStream, builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data!.isEmpty) { return const Center(child: Text('暂无数据')); } final dataList = snapshot.data!; return SfCartesianChart( primaryXAxis: DateTimeAxis( title: const AxisTitle(text: '时间'), labelFormat: 'HH:mm:ss', // 按需调整时间显示格式 ), primaryYAxis: const NumericAxis(title: AxisTitle(text: '数值')), series: <LineSeries<SensorData, DateTime>>[ LineSeries<SensorData, DateTime>( dataSource: dataList, xValueMapper: (SensorData data, _) => data.timestamp, yValueMapper: (SensorData data, _) => data.temperature, name: '温度', markerSettings: const MarkerSettings(isVisible: true), ), // 可添加其他传感器数据折线,比如PH值 LineSeries<SensorData, DateTime>( dataSource: dataList, xValueMapper: (SensorData data, _) => data.timestamp, yValueMapper: (SensorData data, _) => data.ph, name: 'PH值', markerSettings: const MarkerSettings(isVisible: true), ), ], legend: const Legend(isVisible: true), tooltipBehavior: TooltipBehavior(enable: true), ); }, )
核心问题说明
- X轴无数据:大概率是时间戳未转换为
DateTime类型,或xValueMapper未指向正确的时间字段; - 折线不连续:
onChildAdded返回单条数据,必须累加为列表才能形成连续折线,否则图表只会显示最新的单个点。
二、替代方案:使用fl_chart实现折线图
如果Syncfusion配置仍有问题,可尝试fl_chart库,实现示例如下:
1. 添加依赖
在pubspec.yaml中加入:
dependencies: fl_chart: ^0.65.0
2. 构建折线图
复用之前的sensorDataStream流,在StreamBuilder中实现:
StreamBuilder<List<SensorData>>( stream: sensorDataStream, builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data!.isEmpty) { return const Center(child: Text('暂无数据')); } final dataList = snapshot.data!; // 转换为fl_chart所需的点格式 final temperatureSpots = dataList.map((data) { return FlSpot( data.timestamp.millisecondsSinceEpoch.toDouble(), data.temperature, ); }).toList(); final phSpots = dataList.map((data) { return FlSpot( data.timestamp.millisecondsSinceEpoch.toDouble(), data.ph, ); }).toList(); return LineChart( LineChartData( gridData: const FlGridData(show: true), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { // 将时间戳转为可读格式 final time = DateTime.fromMillisecondsSinceEpoch(value.toInt()); return Text( '${time.hour}:${time.minute}:${time.second}', style: const TextStyle(fontSize: 10), ); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: true), ), ), borderData: FlBorderData(show: true), lineBarsData: [ LineChartBarData( spots: temperatureSpots, isCurved: true, barWidth: 2, color: Colors.red, dotData: const FlDotData(show: true), ), LineChartBarData( spots: phSpots, isCurved: true, barWidth: 2, color: Colors.blue, dotData: const FlDotData(show: true), ), ], tooltipData: LineTooltipData( getTooltipItems: (touchedSpots) { return touchedSpots.map((spot) { final time = DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()); return LineTooltipItem( '${spot.y.toStringAsFixed(2)}\n${time.hour}:${time.minute}:${time.second}', const TextStyle(color: Colors.white), ); }).toList(); }, ), ), ); }, )
内容的提问来源于stack exchange,提问作者Erick Mahardika
相关产品推荐
相关产品推荐

