欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net

背景

关于统计数据的展示,图表时避免不了的。所以这里联系学习一下Flutter中关于图表展示功能的实现。

方案咨询

可选方案概览

  • fl_chart: 纯 Dart、支持折线/柱状/饼图/雷达等,动画与样式丰富,集成简单,社区活跃。

  • syncfusion_flutter_charts: 功能最全(海量图表类型、交互、导出、图例、数据缩放等),企业级、性能优,商业许可/社区许可策略需注意。

  • charts_flutter (Google charts): 曾由 Google 提供,API 结构化但维护缓慢、样式定制有限。

  • charts_painter / chart_sparkline / mp_chart_flutter: 轻量或专注单一图表(如微图、Sparkline、基于 MPAndroidChart 的移植),适合特定场景。

  • 自绘 (CustomPainter): 最高自由度与最小依赖,适合极定制或超轻量场景,但工作量大、需处理交互/动画/性能。

  • WebView + JS 图表 (Chart.js / ECharts / D3 等): 可复用成熟 JS 可视化库,适合复杂可视化或已有 JS 逻辑,但引入 WebView、桥接和样式隔离成本。

选择fl_chart方案的理由:

选择 fl_chart 的原因

  • 易用且快速集成:API 直观,上手快——适合在现有页面快速展示饼图(少样式/交互改造)。

  • 纯 Dart/跨平台:无需平台通道或 WebView,兼容移动和桌面。

  • 视觉与动画支持好:默认样式美观,饼图标签、动画、区域样式容易配置。

  • 体积与依赖友好:相比企业级库更轻量,无复杂许可限制(适合开源/个人项目)。

  • 与当前代码契合:我们的统计只是按 branch 计数并展示占比,fl_chart 足够且实现速度最快。

什么时候改用其它库

  • 需要高级交互、导出、海量数据或企业支持 → 考虑 syncfusion_flutter_charts

  • 追求极致定制且愿意投入实现成本 → 采用 CustomPainter

  • 想复用复杂现有 JS 图形或做非常复杂的可视化 → 使用 WebView + ECharts/Chart.js。

修改要点:

pubspec.yaml

追加对fl_chart的依赖

diff --git a/pubspec.yaml b/pubspec.yaml
index e619a21..91427ee 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -40,6 +40,7 @@ dependencies:
   shared_preferences: ^2.1.1
   flutter_secure_storage: ^8.0.0
   provider: ^6.0.5
+  fl_chart: ^1.1.1^M
   # shared_preferences:
   #   git:
   #     url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"

这里一开始遇到了fl_chart版本兼容性问题,如下所示错误

PS D:\flutter\projects\pipeline> flutter run -d chrome
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
Launching lib\main.dart on Chrome in debug mode...
../../pub/hosted/pub.dev/fl_chart-0.60.0/lib/src/utils/utils.dart:265:20: Error: Member not found:
'MediaQuery.boldTextOverride'.
    if (MediaQuery.boldTextOverride(context)) {
                   ^^^^^^^^^^^^^^^^
Waiting for connection from debug service on Chrome...             11.3s
Failed to compile application.
PS D:\flutter\projects\pipeline> 

当让AI去解决的时候,它就开始误入歧途了,各种尝试,各种失败,甚至要放弃fl_chart的方案:(

其实很简单,就是修改为使用最新的版本1.1.1版本就可以了。具体的版本信息和支持情况可以去pub.dev的fl_chart链接去查看(https://pub.dev/packages/fl_chart,可以看到最近的更新为4个月前,所以一般功能的支持应该是没有问题的)

原因简述:

  • 错误来自 fl_chart 调用不存在的 API MediaQuery.boldTextOverride(context),说明你当前的 Flutter SDK 与该 fl_chart 版本的期望 API 不匹配(包使用了你 SDK 中没有的成员)。

可选修复(按推荐顺序)

  1. 升级 Flutter 到与 fl_chart 0.60.0 兼容的版本(长期稳妥)。

    • 优点:不改包代码,保持包最新;缺点:可能需要切换 channel / 自定义构建。

  2. 使用 fl_chart 的旧版本(在你的 SDK 上已知可用)。

    • 修改 pubspec.yaml 把 fl_chart: ^0.60.0 换成较老的版本(例如 0.58.x / 0.55.x——在你本地尝试哪个能通过构建)。

    • 然后运行 flutter pub get

  3. 临时修补 fl_chart 源码(快速验证 / 临时解决)——把出现 MediaQuery.boldTextOverride(context) 的地方改为兼容写法:


    • MediaQuery.boldTextOverride(context)
      替换为
      MediaQuery.maybeOf(context)?.boldText ?? false

    • 位置示例(从你的错误):
      .../pub/hosted/pub.dev/fl_chart-0.60.0/lib/src/utils/utils.dart

    • 修改后执行 flutter clean 然后 flutter pub get / flutter run

    • 注意:这是对缓存中包的本地修改,pub upgrade 可能会覆盖它;更稳妥的做法是 fork 包并在 pubspec.yaml 使用 git 依赖。

lib/main.dart

fl_chart package的导入

+import 'package:fl_chart/fl_chart.dart';

将StatisticsPage 类改为继承自StatefulWidget

 // 统计页面
-class StatisticsPage extends StatelessWidget {
+class StatisticsPage extends StatefulWidget {^M
   const StatisticsPage({super.key});

+  @override^M
+  State<StatisticsPage> createState() => _StatisticsPageState();^M
+}^M

_StatisticsPageState类的追加

  1. 首先是页面显示时对数据的加载,其实这一步应该可以和第一个tab页面共享所加载的数据,不需要再单独去网络请求一次了。
  2. 饼图已经对应标签的生成, 主要使用到了PieChartSectionData, PieChart, PieChartData, PieTouchData等类

     具体实现请查看下面代码diff

+class _StatisticsPageState extends State<StatisticsPage> {^M
+  bool _loading = true;^M
+  String? _error;^M
+  Map<String, int> _counts = {};^M
+  int _total = 0;^M
+^M
+  @override^M
+  void initState() {^M
+    super.initState();^M
+    WidgetsBinding.instance.addPostFrameCallback((_) => _loadStats());^M
+  }^M
+^M
+  Future<void> _loadStats() async {^M
+    setState(() {^M
+      _loading = true;^M
+      _error = null;^M
+    });^M
+^M
+    try {^M
+      final settings = Provider.of<Settings>(context, listen: false);^M
+      final items = await getPipelinesPage(settings.projectID, settings.domain, settings.token, 1, settings.perPage);^M
+      final Map<String, int> map = {};^M
+      for (var p in items) {^M
+        map[p.ref] = (map[p.ref] ?? 0) + 1;^M
+      }^M
+      setState(() {^M
+        _counts = map;^M
+        _total = items.length;^M
+      });^M
+    } catch (e) {^M
+      setState(() {^M
+        _error = e.toString();^M
+      });^M
+    } finally {^M
+      setState(() {^M
+        _loading = false;^M
+      });^M
+    }^M
+  }^M
+^M
+  List<PieChartSectionData> _sections() {^M
+    final colors = Colors.primaries;^M
+    final sections = <PieChartSectionData>[];^M
+    if (_total == 0) return sections;^M
+    int i = 0;^M
+    _counts.forEach((branch, count) {^M
+      final value = count.toDouble();^M
+      final percent = (count / _total) * 100;^M
+      sections.add(PieChartSectionData(^M
+        value: value,^M
+        color: colors[i % colors.length].withOpacity(0.9),^M
+        title: '${percent.toStringAsFixed(0)}%',^M
+        radius: 60,^M
+        titleStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white),^M        
+      ));^M
+      i += 1;^M
+    });^M
+    return sections;^M
+  }^M
+^M
   @override
   Widget build(BuildContext context) {
-    return const Center(
+    if (_loading) return const Center(child: CircularProgressIndicator());^M
+    if (_error != null) return Center(child: Text('加载统计失败:$_error'));^M
+    if (_total == 0) return const Center(child: Text('暂无数据'));^M
+^M
+    final entries = _counts.entries.toList();^M
+    final sections = _sections();^M
+^M
+    return Padding(^M
+      padding: const EdgeInsets.all(16.0),^M
       child: Column(
-        mainAxisAlignment: MainAxisAlignment.center,
         children: [
-          Icon(Icons.bar_chart, size: 64, color: Colors.grey),
-          SizedBox(height: 16),
-          Text('统计页面', style: TextStyle(fontSize: 18)),
-          SizedBox(height: 8),
-          Text('功能开发中...', style: TextStyle(color: Colors.grey)),
+          const Text('分支占比', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),^M
+          const SizedBox(height: 12),^M
+          Expanded(^M
+            child: Row(^M
+              children: [^M
+                Expanded(^M
+                  child: AspectRatio(^M
+                    aspectRatio: 1,^M
+                    child: PieChart(^M
+                      PieChartData(^M
+                        sections: sections,^M
+                        centerSpaceRadius: 36,^M
+                        sectionsSpace: 2,^M
+                        pieTouchData: PieTouchData(enabled: true),^M
+                      ),^M
+                    ),^M
+                  ),^M
+                ),^M
+                const SizedBox(width: 12),^M
+                Expanded(^M
+                  child: ListView.builder(^M
+                    itemCount: entries.length,^M
+                    itemBuilder: (context, index) {^M
+                      final e = entries[index];^M
+                      final percent = (e.value / _total) * 100;^M
+                      final color = Colors.primaries[index % Colors.primaries.length];^M
+                      return ListTile(^M
+                        leading: CircleAvatar(backgroundColor: color, radius: 8),^M
+                        title: Text(e.key),^M
+                        trailing: Text('${e.value} (${percent.toStringAsFixed(0)}%)'),^M
+                      );^M
+                    },^M
+                  ),^M
+                ),^M
+              ],^M
+            ),^M
+          ),^M
+          ElevatedButton(onPressed: _loadStats, child: const Text('刷新')),^M
         ],
       ),
     );

但是同样因为在我的页面中使用了需要鸿蒙适配的包依赖,还没有解决,鸿蒙上还不能正常显示。

下面分别为Android, 鸿蒙模拟器,及Chrom浏览器上的截图,供参考。

总结

1, flutter 应用中的图表生成可以使用fl_chart插件,方便易上手。

2. AI工具使用很方便, 但是也有找错方向的时候,就会浪费时间资源。所以还是需要加强自身知识的学习,这样可以检查确认AI的修改,防止错误或者加强与AI的合作更高效的完成工作。

3. 等插件适配文件解决了之后再来更新鸿蒙截屏图片。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐