OpenHarmony Flutter 数据可视化实战:从图表到分布式数据看板
通过TouchData实现图表交互,点击数据点 / 柱状图显示详细信息。dart@overridefinal List<String> weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];String?double?@overrideappBar: AppBar(title: const Text('交互式销售额趋势')),// 显示选中的数
引言:数据可视化是解读数据的 “直观语言”
在开源鸿蒙(OpenHarmony)Flutter 应用中,数据可视化将枯燥的数字转化为直观的图表、仪表盘,帮助用户快速理解数据趋势和核心指标 —— 从个人健康数据统计,到企业级数据看板,数据可视化是提升应用价值的重要手段。
本文将以 “场景化实战” 为核心,从基础的图表库选型、常见图表实现,到进阶的交互式图表、自定义图表样式,再到开源鸿蒙特有的分布式数据看板,用 “原理 + 精简代码 + 多设备适配” 的方式,带你打造适配全场景的数据可视化方案。
一、图表库选型:FlChart vs charts_flutter
开源鸿蒙 Flutter 应用开发中,常用的数据可视化库有FlChart和charts_flutter,两者各有优势:
| 维度 | FlChart | charts_flutter |
|---|---|---|
| 学习成本 | 中(API 清晰,文档丰富) | 中(基于 Material Design,风格统一) |
| 自定义程度 | 高(支持细节样式自定义) | 中(样式相对固定,定制需额外开发) |
| 性能 | 优秀(渲染高效,适合大数据) | 较好(大数据场景需优化) |
| 图表类型 | 丰富(折线图、柱状图、饼图等) | 丰富(支持地图、热力图等特殊图表) |
| 开源鸿蒙适配 | 良好(无原生依赖,直接使用) | 良好(需确保版本兼容) |
选型建议:
- 需高度自定义样式、追求渲染性能 → 选择 FlChart;
- 偏好 Material Design 风格、需要特殊图表(如地图) → 选择 charts_flutter;
- 本文以FlChart为例展开,其更适合开源鸿蒙多设备适配场景。
二、基础实战:常见图表实现(FlChart)
2.1 环境配置
yaml
dependencies:
fl_chart: ^0.55.2
2.2 实战 1:折线图(数据趋势展示)
适合展示连续数据的变化趋势(如气温变化、销售额走势)。
dart
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class LineChartPage extends StatelessWidget {
const LineChartPage({super.key});
// 模拟一周销售额数据
final List<FlSpot> salesData = const [
FlSpot(0, 1200), // 周一
FlSpot(1, 1500), // 周二
FlSpot(2, 1100), // 周三
FlSpot(3, 1800), // 周四
FlSpot(4, 1600), // 周五
FlSpot(5, 2000), // 周六
FlSpot(6, 2200), // 周日
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('一周销售额趋势')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: LineChart(
LineChartData(
gridData: const FlGridData(show: true), // 显示网格
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1, // 每隔1个点显示标签
getTitlesWidget: (value, meta) {
const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
return Text(weekDays[value.toInt()]);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 500, // 每隔500显示刻度
getTitlesWidget: (value, meta) => Text('${value.toInt()}元'),
),
),
),
borderData: FlBorderData(show: true), // 显示边框
minX: 0,
maxX: 6,
minY: 0,
maxY: 2500,
lineBarsData: [
LineChartBarData(
spots: salesData,
isCurved: true, // 曲线平滑
color: Colors.blue,
width: 3,
dotData: const FlDotData(show: true), // 显示数据点
belowBarData: BarAreaData(
show: true,
color: Colors.blue.withOpacity(0.1), // 填充下方区域
),
),
],
),
),
),
);
}
}
2.3 实战 2:柱状图(分类数据对比)
适合展示不同分类数据的对比(如各部门业绩、产品销量)。
dart
class BarChartPage extends StatelessWidget {
const BarChartPage({super.key});
// 模拟各部门业绩数据
final List<double> departmentPerformance = [85, 68, 92, 76, 88];
final List<String> departments = ['研发部', '市场部', '销售部', '财务部', '人事部'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('各部门季度业绩')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: BarChart(
BarChartData(
gridData: const FlGridData(show: false),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) => Text(departments[value.toInt()]),
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 20,
getTitlesWidget: (value, meta) => Text('${value.toInt()}分'),
),
),
),
borderData: FlBorderData(show: true),
minY: 0,
maxY: 100,
barGroupsSpace: 20, // 柱状图组间距
barTouchData: BarTouchData(enabled: true), // 支持点击交互
barData: [
BarChartGroupData(
showingTooltipIndicators: [0, 1, 2, 3, 4],
barRods: departmentPerformance.map((performance) {
return BarChartRodData(
toY: performance,
color: performance >= 90 ? Colors.green : Colors.blue, // 90分以上绿色
width: 20,
borderRadius: BorderRadius.circular(4),
);
}).toList(),
),
],
),
),
),
);
}
}
2.4 实战 3:饼图(占比数据展示)
适合展示各部分占总体的比例(如收入构成、用户来源分布)。
dart
class PieChartPage extends StatelessWidget {
const PieChartPage({super.key});
// 模拟用户来源分布数据
final List<PieChartSectionData> userSourceData = [
PieChartSectionData(
value: 45,
color: Colors.blue,
title: 'APP商店',
titleStyle: const TextStyle(color: Colors.white, fontSize: 14),
),
PieChartSectionData(
value: 30,
color: Colors.green,
title: '社交媒体',
titleStyle: const TextStyle(color: Colors.white, fontSize: 14),
),
PieChartSectionData(
value: 15,
color: Colors.orange,
title: '搜索引擎',
titleStyle: const TextStyle(color: Colors.white, fontSize: 14),
),
PieChartSectionData(
value: 10,
color: Colors.purple,
title: '其他渠道',
titleStyle: const TextStyle(color: Colors.white, fontSize: 14),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('用户来源分布')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: PieChart(
PieChartData(
sections: userSourceData,
sectionsSpace: 2, // 各部分间距
centerSpaceRadius: 60, // 中心空白半径
pieTouchData: PieTouchData(enabled: true), // 支持点击交互
titleData: const FlTitleData(
show: true,
title: Text('用户来源占比', style: TextStyle(fontSize: 18)),
),
),
),
),
),
);
}
}
三、进阶实战:交互式图表与自定义样式
3.1 交互式图表(点击显示详情)
通过TouchData实现图表交互,点击数据点 / 柱状图显示详细信息。
dart
class InteractiveLineChartPage extends StatefulWidget {
const InteractiveLineChartPage({super.key});
@override
State<InteractiveLineChartPage> createState() => _InteractiveLineChartPageState();
}
class _InteractiveLineChartPageState extends State<InteractiveLineChartPage> {
final List<FlSpot> salesData = const [
FlSpot(0, 1200),
FlSpot(1, 1500),
FlSpot(2, 1100),
FlSpot(3, 1800),
FlSpot(4, 1600),
FlSpot(5, 2000),
FlSpot(6, 2200),
];
final List<String> weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
String? _selectedDay;
double? _selectedSales;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('交互式销售额趋势')),
body: Column(
children: [
// 显示选中的数据详情
if (_selectedDay != null && _selectedSales != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'$_selectedDay 销售额:${_selectedSales?.toInt()}元',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: LineChart(
LineChartData(
gridData: const FlGridData(show: true),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) => Text(weekDays[value.toInt()]),
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 500,
getTitlesWidget: (value, meta) => Text('${value.toInt()}元'),
),
),
),
borderData: FlBorderData(show: true),
minX: 0,
maxX: 6,
minY: 0,
maxY: 2500,
lineBarsData: [
LineChartBarData(
spots: salesData,
isCurved: true,
color: Colors.blue,
width: 3,
dotData: const FlDotData(show: true),
),
],
// 交互配置
lineTouchData: LineTouchData(
enabled: true,
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (touchedSpots) {
return touchedSpots.map((spot) {
final day = weekDays[spot.x.toInt()];
final sales = spot.y.toInt();
// 更新选中状态
setState(() {
_selectedDay = day;
_selectedSales = spot.y;
});
return LineTooltipItem(
'$day: $sales元',
const TextStyle(color: Colors.white),
);
}).toList();
},
),
),
),
),
),
),
],
),
);
}
}
3.2 自定义图表样式(适配开源鸿蒙主题)
结合开源鸿蒙系统主题,自定义图表颜色、字体,实现风格统一。
dart
class ThemedBarChartPage extends StatelessWidget {
const ThemedBarChartPage({super.key});
final List<double> data = [85, 68, 92, 76, 88];
final List<String> labels = ['研发部', '市场部', '销售部', '财务部', '人事部'];
@override
Widget build(BuildContext context) {
// 获取开源鸿蒙系统主题色
final theme = Theme.of(context);
final primaryColor = theme.primaryColor;
final secondaryColor = theme.colorScheme.secondary;
return Scaffold(
appBar: AppBar(title: const Text('主题化部门业绩图表')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: BarChart(
BarChartData(
gridData: FlGridData(
show: true,
gridColor: theme.dividerColor.withOpacity(0.3), // 网格色适配主题
),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) => Text(
labels[value.toInt()],
style: theme.textTheme.bodyMedium, // 字体适配主题
),
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 20,
getTitlesWidget: (value, meta) => Text(
'${value.toInt()}分',
style: theme.textTheme.bodyMedium,
),
),
),
),
borderData: FlBorderData(
show: true,
borderColor: theme.dividerColor,
),
minY: 0,
maxY: 100,
barData: [
BarChartGroupData(
barRods: data.map((value) {
return BarChartRodData(
toY: value,
// 自定义颜色:优秀(≥90)用主题强调色,良好用主题主色
color: value >= 90 ? secondaryColor : primaryColor,
width: 20,
borderRadius: BorderRadius.circular(4),
// 柱状图渐变效果
gradient: LinearGradient(
colors: [
value >= 90 ? secondaryColor.withOpacity(0.8) : primaryColor.withOpacity(0.8),
value >= 90 ? secondaryColor : primaryColor,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
);
}).toList(),
),
],
),
),
),
);
}
}
四、高阶实战:开源鸿蒙分布式数据看板
4.1 分布式数据看板核心逻辑
开源鸿蒙分布式数据看板实现 “多设备数据实时同步 + 可视化展示”,核心流程:
- 数据采集:源设备(如手机)采集数据(如销售数据、健康数据);
- 数据同步:通过分布式存储将数据实时同步到其他设备(如平板、智慧屏);
- 可视化渲染:各设备监听分布式存储变化,实时更新图表展示;
- 多设备适配:根据设备屏幕尺寸(手机→智慧屏)调整图表布局和大小。
4.2 实战:分布式销售数据看板
步骤 1:分布式数据同步工具类
dart
// distributed_store_util.dart
import 'package:ohos_shared_preferences/ohos_shared_preferences.dart';
import 'dart:convert';
class DistributedStoreUtil {
static final SharedPreferences _prefs = SharedPreferences.getInstance() as SharedPreferences;
// 保存销售数据到分布式存储
static Future<void> saveSalesData(List<double> data) async {
await _prefs.setString('distributed_sales_data', jsonEncode(data));
}
// 获取分布式销售数据
static Future<List<double>> getSalesData() async {
final dataStr = _prefs.getString('distributed_sales_data') ?? '[]';
final List<dynamic> dataList = jsonDecode(dataStr);
return dataList.map((e) => e.toDouble()).toList();
}
// 监听数据变化(回调通知)
static void listenSalesData(void Function(List<double>) callback) {
_prefs.addListener(() async {
final data = await getSalesData();
callback(data);
});
}
}
步骤 2:数据采集端(手机)
dart
class DataCollectionPage extends StatefulWidget {
const DataCollectionPage({super.key});
@override
State<DataCollectionPage> createState() => _DataCollectionPageState();
}
class _DataCollectionPageState extends State<DataCollectionPage> {
final List<double> _salesData = [1200, 1500, 1100, 1800, 1600, 2000, 2200];
int _currentDay = 0;
// 模拟实时更新数据(每天增加随机销售额)
void _updateData() {
if (_currentDay < 7) {
setState(() {
_salesData[_currentDay] += (100 + Random().nextInt(300)).toDouble();
});
// 同步到分布式存储
DistributedStoreUtil.saveSalesData(_salesData);
_currentDay++;
// 3秒后更新下一天数据
Future.delayed(const Duration(seconds: 3), _updateData);
}
}
@override
void initState() {
super.initState();
// 初始化时同步初始数据
DistributedStoreUtil.saveSalesData(_salesData);
// 启动模拟数据更新
Future.delayed(const Duration(seconds: 2), _updateData);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('数据采集端(手机)')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text('实时销售数据采集', style: TextStyle(fontSize: 18)),
const SizedBox(height: 20),
Expanded(
child: LineChart(
LineChartData(
gridData: const FlGridData(show: true),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) {
const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
return Text(weekDays[value.toInt()]);
},
),
),
),
borderData: FlBorderData(show: true),
minX: 0,
maxX: 6,
minY: 1000,
maxY: 3000,
lineBarsData: [
LineChartBarData(
spots: _salesData.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), e.value);
}).toList(),
isCurved: true,
color: Colors.blue,
width: 3,
dotData: const FlDotData(show: true),
),
],
),
),
),
],
),
),
);
}
}
步骤 3:数据展示端(平板 / 智慧屏)
dart
class DistributedDashboardPage extends StatefulWidget {
const DistributedDashboardPage({super.key});
@override
State<DistributedDashboardPage> createState() => _DistributedDashboardPageState();
}
class _DistributedDashboardPageState extends State<DistributedDashboardPage> {
List<double> _salesData = [];
final List<String> _weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
@override
void initState() {
super.initState();
// 初始化获取数据
_initData();
// 监听分布式数据变化
DistributedStoreUtil.listenSalesData((data) {
setState(() {
_salesData = data;
});
});
}
Future<void> _initData() async {
final data = await DistributedStoreUtil.getSalesData();
setState(() {
_salesData = data;
});
}
@override
Widget build(BuildContext context) {
// 根据屏幕宽度适配布局:手机(单列)、平板/智慧屏(双列)
final isLargeScreen = MediaQuery.of(context).size.width > 600;
return Scaffold(
appBar: AppBar(title: const Text('分布式数据看板(平板/智慧屏)')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: isLargeScreen
? Row(
children: [
// 左侧折线图(趋势)
Expanded(child: _buildLineChart()),
const SizedBox(width: 20),
// 右侧柱状图(对比)
Expanded(child: _buildBarChart()),
],
)
: Column(
children: [
Expanded(child: _buildLineChart()),
const SizedBox(height: 20),
Expanded(child: _buildBarChart()),
],
),
),
);
}
// 构建折线图
Widget _buildLineChart() {
if (_salesData.isEmpty) return const Center(child: Text('加载中...'));
return LineChart(
LineChartData(
gridData: const FlGridData(show: true),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) => Text(_weekDays[value.toInt()]),
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 500,
getTitlesWidget: (value, meta) => Text('${value.toInt()}元'),
),
),
),
borderData: FlBorderData(show: true),
minX: 0,
maxX: 6,
minY: 1000,
maxY: 3000,
lineBarsData: [
LineChartBarData(
spots: _salesData.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), e.value);
}).toList(),
isCurved: true,
color: Colors.blue,
width: 3,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(show: true, color: Colors.blue.withOpacity(0.1)),
),
],
),
);
}
// 构建柱状图
Widget _buildBarChart() {
if (_salesData.isEmpty) return const Center(child: Text('加载中...'));
return BarChart(
BarChartData(
gridData: const FlGridData(show: false),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) => Text(_weekDays[value.toInt()]),
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 500,
getTitlesWidget: (value, meta) => Text('${value.toInt()}元'),
),
),
),
borderData: FlBorderData(show: true),
minY: 1000,
maxY: 3000,
barData: [
BarChartGroupData(
barRods: _salesData.map((value) {
return BarChartRodData(
toY: value,
color: Colors.green,
width: 20,
borderRadius: BorderRadius.circular(4),
);
}).toList(),
),
],
),
);
}
}
五、多设备适配与性能优化
5.1 多设备适配技巧
- 屏幕尺寸适配:通过
MediaQuery获取屏幕宽度,动态调整图表布局(单列 / 双列)、大小和字体; - 设备类型适配:针对手机(小屏)简化图表细节(如隐藏网格、减小字体),针对智慧屏(大屏)增加图表维度和交互细节;
- 系统主题适配:复用开源鸿蒙系统主题色、字体,确保图表与系统风格统一。
5.2 性能优化(大数据场景)
- 数据采样:当数据量超过 1000 条时,对数据进行采样(如每隔 5 条取 1 条),减少渲染压力;
- 懒加载:图表初始化时只加载部分数据,滚动时再加载剩余数据;
- 避免频繁重建:使用
StatefulWidget缓存图表数据,仅在数据变化时触发重建; - 硬件加速:确保启用 Flutter 硬件加速,提升图表渲染帧率。
六、常见问题(FAQ)

Q1:图表在开源鸿蒙智慧屏上显示模糊怎么办?
A1:1. 设置图表width和height为具体数值(而非百分比),确保适配大屏分辨率;2. 增加图表字体大小和线条宽度;3. 关闭图表缩放功能,避免拉伸模糊。
Q2:分布式数据同步延迟如何解决?
A2:1. 优化数据格式(如使用 JSON 而非 XML,减少数据体积);2. 减少同步频率(如批量同步而非单条同步);3. 优先使用鸿蒙分布式网络(如 Wi-Fi Direct),提升传输速度。
Q3:如何实现 3D 图表效果?
A3:FlChart 本身不支持 3D 效果,可通过以下方式实现:1. 使用Transform添加透视变换;2. 自定义BarChartRodData的渐变和阴影,模拟 3D 立体感;3. 集成第三方 3D 图表库(如flutter_3d_charts)。
结语:构建开源鸿蒙全场景数据可视化体系
数据可视化是连接数据与用户的桥梁,从基础的折线图、柱状图,到交互式图表、分布式数据看板,每个场景都需要结合设备特性和业务需求进行设计。
通过本文的实战案例,你已经掌握了 FlChart 的核心用法、交互式图表开发、分布式数据同步和多设备适配技巧,能够在开源鸿蒙手机、平板、智慧屏等设备上实现高效、美观的数据可视化效果。合理运用数据可视化,能让你的应用在全场景生态中更具竞争力,为用户提供更直观的数据解读体验。
更多推荐

所有评论(0)