Flutter柱状图实战:用fl_chart从入门到性能优化
发布时间:2026/10/20 19:53:37 锦皓数字建站

做 Flutter 开发的人迟早会撞上一个需求往页面上塞一张柱状图。我第一次图省事找了一堆图形库结果要么太重要么 API 反人类。后来换了 fl_chart把 BarChart 从零到一跑通之后基本就再没换过别的。这篇文章聊聊我用 fl_chart 画柱状图的那点心得适合刚从 ListView 毕业、想在项目里加图表的新手也适合已经接入了但想少踩点坑的中级开发者。全文不涉及复杂的前后端架构只说 BarChart 这一个组件怎么用顺手。1. 为什么柱状图要选 fl_chart 而不是自己画1.1 先说说我在项目里遇到的图表需求图表这个需求看着简单做起来全是细节。我接过一个内部报表页要求展示某业务在过去七天的数据变化每根柱子要有颜色区分、顶部要显示具体数值、点击柱子还要弹出详细说明。如果拿 CustomPainter 硬画绘制逻辑、触摸命中的坐标换算、文字排布、动画过渡都得自己处理一套下来没个两三天搞不定还容易在 iOS 和 Android 上出现渲染差异。用 fl_chart 的 BarChart核心代码几十行就成型了剩下时间都在调样式。1.2 fl_chart 和别的图表库比赢在哪Flutter 生态里图表库其实不少但 fl_chart 是维护最活跃、文档和示例最完整的那一批。它把所有图表拆成独立 WidgetBarChart 就是其中之一用法和环境没有强耦合。它的 API 设计是数据驱动的你把数据模型准备好剩下的布局、动画、触摸响应交给图表本身。最打动我的细节是它自带动画过渡数据刷新时柱子会自然地从旧值滑到新值这个效果在 CustomPainter 里需要自己写插值在 fl_chart 里是默认行为。1.3 什么情况下可以直接画而不是用库不要因为上了 fl_chart 就把所有图都往它身上塞。如果只是画两三个静态矩形比如简单的进度条变体那直接放几个 Container 都够了如果需求是复杂的自定义图形比如需要标注任意多边形、自由绘制热力图那 CustomPainter 反而更灵活。fl_chart 适合标准图表类型尤其是柱状图、折线图、饼图这种看得见、摸得着的常规形态。我在项目里总结的判断标准是如果图表的数据结构能用一维列表描述清楚用 fl_chart 通常是最省事的选择。2. 从依赖引入到第一个柱状图跑通2.1 版本怎么选避免装完就冲突fl_chart 的版本迭代很快不同版本的 API 有细微差异。我在 pubspec.yaml 里一般直接写fl_chart: ^0.66.0安装后先跑一个最小示例确认编译通过再开始写业务代码。如果项目里还用了老版本的 JSON 解析库或绘图库fl_chart 和它们基本没有共享依赖冲突概率很低但要注意 Flutter SDK 的最低版本要求。如果你用的 Flutter 版本偏旧建议先看官方 changelog不要盲目拉最新版。开发中的提醒如果不确认当前环境该用哪个版本可以建一个干净项目先装上 fl_chart编译通过后再复制业务代码过去。这样能避免版本问题混进你的逻辑排查里。2.2 最小可运行代码三条数据画一张图拿到库之后第一件要做的事不是看高深用法而是把一张图画出来。下面这个例子是我的起点用三根柱子代表三天数据。你复制到项目里就能跑。import package:flutter/material.dart; import package:fl_chart/fl_chart.dart; class SimpleBarChartPage extends StatelessWidget { const SimpleBarChartPage({super.key}); override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(fl_chart 柱状图示例)), body: Padding( padding: const EdgeInsets.all(16), child: BarChart( BarChartData( barGroups: [ BarChartGroupData(x: 0, barRods: [ BarChartRodData(toY: 3, color: Colors.blue), ]), BarChartGroupData(x: 1, barRods: [ BarChartRodData(toY: 5, color: Colors.orange), ]), BarChartGroupData(x: 2, barRods: [ BarChartRodData(toY: 7, color: Colors.green), ]), ], ), ), ), ); } }这段代码不需要手动设置坐标轴fl_chart 会按数据自动为 Y 轴生成整数刻度。运行后你就能看到三根颜色不同的柱子。很多教程会让你从这里开始但如果你脑子里的问题已经是怎么让柱子的文字显示出来建议直接往下看数据模型。2.3 图表不显示的常见原因等我真的把这段代码写进项目时遇到的最常见问题是图表区域是空白连坐标轴都没有。原因通常是BarChartData被写成了空对象或者barGroups列表根本没有数据。还有一坑是放在了SingleChildScrollView里且没有给高度约束BarChart 默认会填满父级约束如果父级高度无限它绘制时就会直接跳过。解决方法很简单给 BarChart 包一个固定高度的 Container比如SizedBox(height: 300)。另一个诡异情况是柱子显示但 Y 轴刻度奇怪不是从 0 开始。fl_chart 会基于数据自动计算最小值和最大值如果你希望它强制从 0 开始需要手动指定minY和maxY。这两种问题的排查路径完全不同建议先确认是没画出来还是画出来了不符合预期。3. BarChart 的数据模型读懂三层结构3.1 BarChartData 是总指挥很多人第一次接触 fl_chart会被一堆类名绕晕。其实记住三层结构就够了最外层是BarChartData它决定图表的整体行为包括对齐方式、纵轴范围、格线、Tooltip 等。中间层是BarChartGroupData它代表一组柱子。最内层是BarChartRodData它描述一根具体的柱子从底部到顶部的长度、颜色和宽度。举例来说你展示周一至周日的销售额每天就是一个BarChartGroupData如果某一天有两个品类要对比那这一组里就有两个barRods。BarChartData里最需要关心的几个字段barGroups接收一个列表alignment控制组之间对齐方式一般用BarChartAlignment.spaceAround让柱子间距自然titlesData配置坐标轴标签barTouchData控制点击交互。搞懂这四块BarChart 的大部分能力就已经在手里了。3.2 BarChartGroupData 负责分组BarChartGroupData的核心是x坐标和barRods列表。x不是柱子的横坐标像素而是逻辑坐标你给它 0、1、2图表会按顺序把它们排开。barRods中的数据会对应地渲染在同一组内。如果要做分组柱状图最常见的写法是把多个BarChartRodData放进同一个BarChartGroupData。比如展示两个月份的销量对比时每个月份一个分组组内两根柱子分别代表产品 A 和产品 B。还需要配合barWidth和groupsSpace来调节组与组、柱与柱之间的间距否则视觉上会挤成一团。3.3 BarChartRodData 决定每根柱子的长相到了BarChartRodData这一层你能控制的细节非常多。toY是柱子顶部对齐的数值color设颜色width设柱宽borderRadius设顶部圆角还可以用backDrawRodData给柱子加一个背景柱做参考线。我最常用的组合是设置一个渐变颜色gradient属性可以定义多个颜色之间的过渡视觉效果明显比纯色好很多尤其在海报页上。有一点值得注意toY支持负数柱图会从零线向下绘制。这在做盈亏报表时很有用。实现时只需要给对应的BarChartRodData设置toY: -2图表会自动扩展 Y 轴范围并向下绘制。要是没提前把minY设置为负数你可能只会看到一根向下延伸到画布外的柱子。4. 让柱状图不再难看的几个样式细节4.1 坐标轴与刻度标签默认的坐标轴很朴素有时候连单位都不显示。我项目中遇到的实际需求是把 Y 轴标签改成人性化单位比如 12 万而不是 1200000。fl_chart 提供了SideTitles配置在AxisTitles里设置。BarChartData( axisTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: (value, meta) { if (value % 10000 ! 0) return const SizedBox.shrink(); return Text(${(value / 10000).toStringAsFixed(0)}万); }, ), ), )这里有几个细节需要注意showTitles不写的话标签默认是隐藏的很多新手第一步就在这里摔跤reservedSize是给标签预留的宽度太小会导致文字溢出绘制区getTitlesWidget里需要自己控制哪些值显示否则每个刻度都显示数字多了会糊成一片。按照上述写法Y 轴上只会在整万位置显示 1万2万 字样其余刻度输出一个空白占位。4.2 柱子的圆角、颜色和宽度想让柱子好看核心是让颜色和圆角配合得当。BarChartRodData的修改是在你准备好数据模型之后最值得调的三个属性是borderRadius、width和gradient。我这里展示一个带渐变的示例BarChartRodData( toY: 6, width: 22, borderRadius: const BorderRadius.vertical(top: Radius.circular(6)), gradient: const LinearGradient( colors: [Color(0xFF6C63FF), Color(0xFFB388FF)], begin: Alignment.bottomCenter, end: Alignment.topCenter, ), )圆角的弧度不要太大否则顶部像一个蘑菇头比例反而奇怪。宽度方面如果 interval 是 1柱宽 22 在大多数手机屏幕上看起来都比较舒适屏幕宽度较大时可以适当上调到 28。需要提醒的是width是柱子的物理像素宽度它不随分组自动缩放。当你在不同分辨率的设备上查看图表时宽柱子在小屏幕上可能相互遮挡。4.3 Tooltip 和柱顶数值柱顶显示数值有两种常见做法一种是在图表上绘制文字另一种是配置 Tooltip 让用户点按时看到。前者用BarTouchData配合tooltipBgColor来实现。fl_chart 的默认 Tooltip 已经很好用只需要在BarTouchData里打开touchTooltipData并设置getTooltipItems回调。BarChartData( barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) { return BarTooltipItem( ${rod.toY}, const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ); }, ), ), )如果只是想要一根柱子被点击时弹出黑色气泡显示数值这个写法就够了。但要注意getTooltipItem返回 null 时这个分组不会弹出 Tooltip所以逻辑分支要写清楚。另一个重复出现的需求是在柱顶永久显示数字fl_chart 没有专门的开关通常做法是用Stack把文本叠加在图表上方或者在getTitlesWidget里绘制一个带颜色的文本但难度不小。5. 点击交互与动态刷新数据的实践5.1 打开 handshake 之后发生了什么BarChart 默认是禁用触摸交互的开了barTouchData之后用户点击某一组柱子时触发的回调会告诉你点击到了哪个坐标组、哪根柱子。这个回调发生在BarTouchData的touchCallback里它拿到的是一个FlTouchEvent和BarTouchResponse。最常见的用法是点击后高亮当前柱子同时更新页面下面的卡片。BarTouchData( touchCallback: (event, response) { if (event is FlTapUpEvent response ! null) { setState(() { selectedIndex response.spot.touchedBarGroupIndex; }); } }, enabled: true, )用setState把点击到的分组索引保存下来再把这些设置为柱子的颜色或 Tooltip 的显示条件就能做到点击切换状态。若不做任何状态更新光是回调打印页面自然没有反应。这里我踩过一个坑enabled不显式设置为 true 时触摸事件不会触发。fl_chart 新版里enabled默认值虽然是 true但如果你自己封装了一个 BarChart 的通用组件还是要显式写出来避免某个版本悄悄变了行为。5.2 用 setState 更新图表数据的注意事项动态刷新数据是图表里最常见的高频操作。我最早实现的是每三秒从接口拉一次数据然后替换barGroups再setState。这个流程跑起来没什么问题但有个隐藏坑如果整个BarChartData每次都被重新创建新旧数据因为对象的引用变化动画会强制重新触发视觉上会有闪烁感。更好的做法是只更新变化的数据结构例如只更新barRods里对应数值和颜色其他配置项保持不变。另外如果你在build方法里临时构造BarChartData每次重建时那些配置对象也会全部重建这虽然不影响功能但会白白消耗性能。把BarChartData写成独立的计算函数尽量缓存不变的部分能明显减少掉帧。性能提醒当你有 20 组以上的数据时build方法里不要直接写一长串对象初始化优先抽成私有方法。这样既方便单元测试也方便你在日志里定位是哪段数据出了问题。6. 我在实际项目中踩过的 BarChart 坑6.1 数据过密时柱子变成一条线第一次遇到这个问题是在某运营后台需要展示 30 天的数据。30 组柱子塞进 300 宽的图表里每组算下来只有 10 像素再加上柱宽设置的 12柱子之间直接重叠整张图看起来就是几根实线。解决办法有两个方向一是增大图表的横向滚动二是在 UI 上降密。fl_chart 在短期内更推荐降密比如每 5 天显示一个刻度柱子只画平均值然后再配合一个可以横向滑动的外层容器。我当时用的是SingleChildScrollView横向包住 BarChart并给 BarChart 设置一个宽于屏幕的固定宽度比如 600。这个方法立竿见影但要注意触摸交互需要让它和滚动容器协同否则拖动手势会被 Tooltip 抢走。更稳妥的做法是限制交互只在点击选柱子模式下使用横向滚动并额外判断手势方向。6.2 空数据崩溃与数据边界数据为空的时候barGroups传一个空列表页面会渲染一个空图表不崩但如果你拿到的是 null直接放进barGroups就会在绘制时触发空指针异常。这个问题的重现概率非常高从接口拿数据时数组字段可能是 null或者解析结果缺省。我在封装的组件里会先做一个兜底final groups data null ? BarChartGroupData[] : data.map(...).toList();还要注意toY全是 0 的情况。图表把所有柱子画成零高度坐标轴刻度也会变得很奇怪。我习惯在空数据或全 0 数据时展示一张占位图而不是让用户看到一坨无意义的空白坐标轴。minY和maxY不要依赖自动计算尽量根据业务范围手动指定尤其在数据波动极小的场景下自动刻度会夸张地拉伸高度差。6.3 性能优化与 RepaintBoundary图表一旦放进列表页性能问题就来了。你滑动列表时 BarChart 会持续重绘如果图表本身还带着动画掉帧会非常明显。偶然发现一个简单的优化用RepaintBoundary把图表包起来它能把图表的绘制隔离在一个独立的图层里不会因为列表滚动触发无关的重绘。RepaintBoundary( child: BarChart( ... ), )还有一个更根本的优化是给BarChartData增加duration控制动画速度。实时刷新数据时把动画时长缩短到 200 毫秒左右视觉上既顺滑又不拖沓但如果你的页面同时有多个图表动画时间越长滚动时的性能损耗越大。我的经验是超过两张图表同时显示时把动画时长设置在 300 毫秒以下必要时直接关掉BarChartData的animationDuration。最后再分享一个实际项目里的习惯把所有图表配置抽成一个函数入参是数据模型出参是BarChartData。测试时可以单独验证配置的正确性也方便团队成员在不知道 fl_chart 内部细节的情况下快速维护。每次改动图表你只需要盯住一个函数不用担心页面里散落着十几个BarChartData实例那种失控的场面。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。