You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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;
  }
}

问题根源分析

  1. 频繁创建FlSpot实例:每次testProvider状态更新,Chart的build方法都会重新执行,_buildLineData会重新生成3×1500个全新的FlSpot对象,而fl_chart内部可能持有旧数据引用,导致这些对象无法被及时回收。
  2. 状态更新过于频繁:悬停移动时,touchCallback会持续触发FlTouchEvent,导致testProvider状态高频更新,进而触发大量组件重建和对象实例化。
  3. 不可变对象重复创建:每次设置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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 03:52:08