使用Riverpod与fl_chart时内存泄漏原因及多图表联动问题排查
多图表同步悬停引发FlChart内存泄漏的解决方法
问题背景
需求:实现多个LineChart组件,悬停其中一个时,所有图表同步显示对应位置的悬停指示器。
实现方案:通过Riverpod的StateProvider存储当前悬停索引,所有Chart组件监听该状态;借助fl_chart的touchCallback捕获交互事件更新状态,各图表根据索引设置showingIndicators实现同步。
出现的问题:悬停操作时内存急剧膨胀,FlSpot对象从初始的4501个(3条线×1500个数据点)快速增长到数百万个,且无法被垃圾回收,引发严重内存泄漏。
疑问:是Riverpod或fl_chart的API使用有误,还是fl_chart存在底层bug?
复现代码
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fl_chart/fl_chart.dart'; void main() { runApp(const ProviderScope(child: MyApp())); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: SafeArea( child: Scaffold( body: Column( children: [ Expanded(child: Chart(),), Expanded(child: Chart(),), ] ) ) ), theme: ThemeData(useMaterial3: true), ); } } final testProvider = StateProvider<int>((ref) => 0); class Chart extends ConsumerWidget { Chart({super.key}); static const int _numberOfLines = 3; static const int _numberOfDataPoints = 1500; @override Widget build(BuildContext context, WidgetRef ref) { final int index = ref.watch(testProvider); return LineChart( LineChartData( lineTouchData: LineTouchData( handleBuiltInTouches: false, touchSpotThreshold: 20, touchCallback: (final FlTouchEvent ev, final LineTouchResponse? res) { ref.read(testProvider.notifier).state = res?.lineBarSpots?.first.spotIndex ?? 0; }, ), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), lineBarsData: _buildLineData(index), ), ); } List<LineChartBarData> _buildLineData(final int index) { final List<LineChartBarData> lineData = <LineChartBarData>[]; for (int i = 0; i < _numberOfLines; ++i) { lineData.add(LineChartBarData( showingIndicators: <int>[index], dotData: FlDotData(show: false), spots: () { return List<FlSpot>.generate( _numberOfDataPoints, (index) { return FlSpot(index.toDouble(), index.toDouble()); } ); }() )); } return lineData; } }
问题根源分析
- 频繁创建FlSpot实例:每次
testProvider状态更新,Chart的build方法都会重新执行,_buildLineData会重新生成3×1500个全新的FlSpot对象,而fl_chart内部可能持有旧数据引用,导致这些对象无法被及时回收。 - 状态更新过于频繁:悬停移动时,
touchCallback会持续触发FlTouchEvent,导致testProvider状态高频更新,进而触发大量组件重建和对象实例化。 - 不可变对象重复创建:每次设置
showingIndicators时都创建新的List<int>[index],进一步增加内存开销。
解决方案
1. 预缓存FlSpot数据
将FlSpot列表提前初始化并复用,避免每次build都重新生成:
class Chart extends ConsumerWidget { Chart({super.key}); static const int _numberOfLines = 3; static const int _numberOfDataPoints = 1500; // 预缓存FlSpot数据 final List<List<FlSpot>> _cachedSpots = List.generate(_numberOfLines, (_) { return List<FlSpot>.generate( _numberOfDataPoints, (index) => FlSpot(index.toDouble(), index.toDouble()), ); }); @override Widget build(BuildContext context, WidgetRef ref) { final int index = ref.watch(testProvider); return LineChart( LineChartData( lineTouchData: LineTouchData( handleBuiltInTouches: false, touchSpotThreshold: 20, touchCallback: (final FlTouchEvent ev, final LineTouchResponse? res) { // 仅在触摸开始/移动且有效时更新状态 if (ev is FlTapDownEvent || ev is FlPanUpdateEvent) { ref.read(testProvider.notifier).state = res?.lineBarSpots?.first.spotIndex ?? 0; } }, ), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), lineBarsData: _buildLineData(index), ), ); } List<LineChartBarData> _buildLineData(final int index) { final List<LineChartBarData> lineData = <LineChartBarData>[]; for (int i = 0; i < _numberOfLines; ++i) { lineData.add(LineChartBarData( showingIndicators: [index], dotData: const FlDotData(show: false), spots: _cachedSpots[i], // 复用预缓存的FlSpot列表 )); } return lineData; } }
2. 防抖处理状态更新
使用防抖函数限制testProvider的更新频率,避免高频状态变更:
// 添加防抖工具函数 typedef DebounceCallback = void Function(); DebounceCallback debounce(Duration duration, DebounceCallback callback) { Timer? timer; return () { timer?.cancel(); timer = Timer(duration, callback); }; } // 在Chart中使用 class Chart extends ConsumerWidget { Chart({super.key}); static const int _numberOfLines = 3; static const int _numberOfDataPoints = 1500; final List<List<FlSpot>> _cachedSpots = List.generate(_numberOfLines, (_) { return List<FlSpot>.generate( _numberOfDataPoints, (index) => FlSpot(index.toDouble(), index.toDouble()), ); }); // 防抖函数,间隔50ms更新一次状态 final _debounceUpdate = debounce(const Duration(milliseconds: 50), () {}); @override Widget build(BuildContext context, WidgetRef ref) { final int index = ref.watch(testProvider); return LineChart( LineChartData( lineTouchData: LineTouchData( handleBuiltInTouches: false, touchSpotThreshold: 20, touchCallback: (final FlTouchEvent ev, final LineTouchResponse? res) { if (ev is FlPanUpdateEvent || ev is FlTapDownEvent) { final newIndex = res?.lineBarSpots?.first.spotIndex ?? 0; // 防抖更新状态 _debounceUpdate(() { ref.read(testProvider.notifier).state = newIndex; }); } }, ), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), lineBarsData: _buildLineData(index), ), ); } // ... 其余代码不变 }
3. 优化组件重建
用Consumer包裹最小化重建范围,仅在索引变化时重建图表:
@override Widget build(BuildContext context, WidgetRef ref) { return Consumer( builder: (context, ref, _) { final int index = ref.watch(testProvider); return LineChart( // ... 图表配置 ); }, ); }
总结
内存泄漏的核心原因是高频状态更新导致的大量重复对象创建,以及fl_chart对旧数据的引用未及时释放。通过预缓存数据、防抖状态更新、优化组件重建这几点,可以有效解决内存膨胀问题。这并非Riverpod或fl_chart的底层bug,而是API使用方式未考虑性能优化导致的。
内容的提问来源于stack exchange,提问作者av4625
相关产品推荐
相关产品推荐

