📚 目录

1. 概述

1.1 什么是 Fl Chart?

fl_chart 是一个强大的 Flutter 图表库,提供了:

  • 丰富的图表类型:折线图、柱状图、饼图、散点图等
  • 高度可定制:支持自定义颜色、样式、动画等
  • 性能优秀:流畅的动画和渲染性能
  • 易于使用:简单的 API,几行代码即可创建图表
  • 跨平台:支持 Android、iOS、Web、HarmonyOS 等平台

1.2 为什么在天气应用中使用 Fl Chart?

在天气应用中,图表可以帮助用户:

功能 说明
📈 温度趋势 直观展示未来几天的温度变化
🌧️ 降水量趋势 清晰显示降水量的分布情况
💧 湿度趋势 帮助用户了解湿度变化
📊 数据可视化 将数据转化为直观的图表

1.3 应用场景

在天气应用中,我们使用 Fl Chart 实现:

  • 📈 温度趋势图表:显示最高温度和最低温度的变化趋势
  • 🌧️ 降水量趋势图表:展示未来几天的降水量分布
  • 💧 湿度趋势图表:显示相对湿度的变化情况
  • 📊 多天预报可视化:支持 3、7、10、15、30 天的预报数据展示

1.4 功能流程图

温度

降水量

湿度

📱 用户打开天气详情页

选择预报天数
3/7/10/15/30天

加载预报数据

数据加载完成?

处理数据
提取温度/降水量/湿度

显示加载动画

构建图表数据
FlSpot/BarChartGroupData

图表类型

创建折线图
LineChart

创建柱状图
BarChart

创建折线图
LineChart

显示最高温度线
橙色

显示最低温度线
蓝色

显示降水量柱状图
蓝色

显示湿度折线图
青色

渲染图表

用户查看趋势


2. 引入三方库

2.1 添加依赖

pubspec.yaml 文件的 dependencies 部分添加:

dependencies:
  flutter:
    sdk: flutter
  
  # Flutter 图表组件
  fl_chart: ^0.69.0

2.2 安装依赖

在项目根目录运行:

flutter pub get

预期输出:

Resolving dependencies...
Downloading packages...
+ fl_chart 0.69.2
Got dependencies!

2.3 依赖说明

依赖包 版本 用途
fl_chart ^0.69.0 Flutter 图表组件核心库,提供图表渲染功能

2.4 导入库

在需要使用图表的文件中导入:

import 'package:fl_chart/fl_chart.dart';

3. 目录结构

3.1 项目结构

lib/
├── widgets/
│   └── weather_trend_chart.dart    # 天气趋势图表组件
├── screens/
│   └── weather_detail_page.dart    # 天气详情页(使用图表)
└── models/
    └── weather_models.dart         # 天气数据模型

3.2 文件说明

  • weather_trend_chart.dart:天气趋势图表组件

    • 封装图表创建逻辑
    • 支持温度、降水量、湿度三种图表类型
    • 提供统一的图表接口
  • weather_detail_page.dart:天气详情页面

    • 集成天气趋势图表
    • 根据选择的预报天数动态更新图表

4. 核心代码解读

4.1 图表组件架构

temperature

precipitation

humidity

WeatherTrendChart组件

接收预报数据
List

选择图表类型
ChartType

处理数据
提取温度/降水量/湿度

图表类型

_buildTemperatureChart
温度折线图

_buildPrecipitationChart
降水量柱状图

_buildHumidityChart
湿度折线图

创建最高温度线
FlSpot列表

创建最低温度线
FlSpot列表

创建降水量数据
BarChartGroupData列表

创建湿度数据
FlSpot列表

LineChart渲染

BarChart渲染

LineChart渲染

4.2 图表组件实现

创建 lib/widgets/weather_trend_chart.dart 文件:

import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../models/weather_models.dart';

/// 天气趋势图表组件
/// 支持显示温度、降水量等天气数据的趋势图表
class WeatherTrendChart extends StatelessWidget {
  /// 预报数据列表
  final List<Daily> dailyList;
  
  /// 图表类型
  final ChartType chartType;
  
  /// 图表高度
  final double height;

  const WeatherTrendChart({
    super.key,
    required this.dailyList,
    this.chartType = ChartType.temperature,
    this.height = 250,
  });

  
  Widget build(BuildContext context) {
    if (dailyList.isEmpty) {
      return const SizedBox.shrink();
    }

    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 16),
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(20),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.04),
            blurRadius: 10,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            _getChartTitle(),
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.w600,
              color: Colors.grey.shade800,
            ),
          ),
          const SizedBox(height: 16),
          SizedBox(
            height: height,
            child: _buildChart(),
          ),
        ],
      ),
    );
  }

  /// 获取图表标题
  String _getChartTitle() {
    switch (chartType) {
      case ChartType.temperature:
        return '📈 温度趋势';
      case ChartType.precipitation:
        return '🌧️ 降水量趋势';
      case ChartType.humidity:
        return '💧 湿度趋势';
    }
  }

  /// 构建图表
  Widget _buildChart() {
    switch (chartType) {
      case ChartType.temperature:
        return _buildTemperatureChart();
      case ChartType.precipitation:
        return _buildPrecipitationChart();
      case ChartType.humidity:
        return _buildHumidityChart();
    }
  }
  
  // ... 其他方法实现
}

关键属性说明:

  • dailyList:预报数据列表,包含多天的天气数据
  • chartType:图表类型(温度、降水量、湿度)
  • height:图表高度,默认 250

4.3 温度趋势图表实现

/// 构建温度趋势图表
Widget _buildTemperatureChart() {
  // 1. 创建最高温度数据点
  final spotsMax = dailyList.asMap().entries.map((entry) {
    final temp = double.tryParse(entry.value.tempMax) ?? 0.0;
    return FlSpot(entry.key.toDouble(), temp);
  }).toList();

  // 2. 创建最低温度数据点
  final spotsMin = dailyList.asMap().entries.map((entry) {
    final temp = double.tryParse(entry.value.tempMin) ?? 0.0;
    return FlSpot(entry.key.toDouble(), temp);
  }).toList();

  // 3. 计算 Y 轴范围
  final maxTemp = dailyList.map((d) => double.tryParse(d.tempMax) ?? 0.0)
      .reduce((a, b) => a > b ? a : b);
  final minTemp = dailyList.map((d) => double.tryParse(d.tempMin) ?? 0.0)
      .reduce((a, b) => a < b ? a : b);
  final tempRange = maxTemp - minTemp;
  final minY = (minTemp - tempRange * 0.1).floor().toDouble();
  final maxY = (maxTemp + tempRange * 0.1).ceil().toDouble();

  // 4. 创建折线图
  return LineChart(
    LineChartData(
      gridData: FlGridData(
        show: true,
        drawVerticalLine: false,
        horizontalInterval: (maxY - minY) / 4,
        getDrawingHorizontalLine: (value) {
          return FlLine(
            color: Colors.grey.shade200,
            strokeWidth: 1,
          );
        },
      ),
      titlesData: FlTitlesData(
        show: true,
        bottomTitles: AxisTitles(
          sideTitles: SideTitles(
            showTitles: true,
            reservedSize: 30,
            interval: 1,
            getTitlesWidget: (value, meta) {
              if (value.toInt() >= 0 && value.toInt() < dailyList.length) {
                final date = dailyList[value.toInt()].fxDate;
                final dateStr = date.split('-');
                return Padding(
                  padding: const EdgeInsets.only(top: 8),
                  child: Text(
                    '${dateStr[1]}/${dateStr[2]}',
                    style: TextStyle(
                      fontSize: 10,
                      color: Colors.grey.shade600,
                    ),
                  ),
                );
              }
              return const Text('');
            },
          ),
        ),
        leftTitles: AxisTitles(
          sideTitles: SideTitles(
            showTitles: true,
            reservedSize: 50,
            interval: (maxY - minY) / 4,
            getTitlesWidget: (value, meta) {
              return Text(
                '${value.toInt()}°',
                style: TextStyle(
                  fontSize: 10,
                  color: Colors.grey.shade600,
                ),
              );
            },
          ),
        ),
      ),
      borderData: FlBorderData(
        show: true,
        border: Border.all(color: Colors.grey.shade300),
      ),
      minX: 0,
      maxX: (dailyList.length - 1).toDouble(),
      minY: minY,
      maxY: maxY,
      lineBarsData: [
        // 最高温度线(橙色)
        LineChartBarData(
          spots: spotsMax,
          isCurved: true,
          color: Colors.orange,
          barWidth: 3,
          isStrokeCapRound: true,
          dotData: FlDotData(
            show: true,
            getDotPainter: (spot, percent, barData, index) {
              return FlDotCirclePainter(
                radius: 4,
                color: Colors.orange,
                strokeWidth: 2,
                strokeColor: Colors.white,
              );
            },
          ),
        ),
        // 最低温度线(蓝色)
        LineChartBarData(
          spots: spotsMin,
          isCurved: true,
          color: Colors.blue,
          barWidth: 3,
          isStrokeCapRound: true,
          dotData: FlDotData(
            show: true,
            getDotPainter: (spot, percent, barData, index) {
              return FlDotCirclePainter(
                radius: 4,
                color: Colors.blue,
                strokeWidth: 2,
                strokeColor: Colors.white,
              );
            },
          ),
        ),
      ],
    ),
  );
}

关键步骤说明:

  1. 数据转换:将 Daily 数据转换为 FlSpot 数据点
  2. Y 轴范围计算:根据数据自动计算合适的 Y 轴范围
  3. 图表配置:配置网格、标题、边框等
  4. 线条数据:创建最高温度和最低温度两条线

image-20260130230625274

4.4 降水量趋势图表实现

/// 构建降水量趋势图表
Widget _buildPrecipitationChart() {
  // 1. 创建降水量数据点
  final spots = dailyList.asMap().entries.map((entry) {
    final precip = double.tryParse(entry.value.precip) ?? 0.0;
    return FlSpot(entry.key.toDouble(), precip);
  }).toList();

  // 2. 计算最大降水量
  final maxPrecip = dailyList.map((d) => double.tryParse(d.precip) ?? 0.0)
      .reduce((a, b) => a > b ? a : b);
  final maxY = maxPrecip > 0 ? (maxPrecip * 1.2).ceil().toDouble() : 10.0;

  // 3. 创建柱状图
  return BarChart(
    BarChartData(
      gridData: FlGridData(
        show: true,
        drawVerticalLine: false,
        horizontalInterval: maxY / 4,
      ),
      titlesData: FlTitlesData(
        show: true,
        bottomTitles: AxisTitles(
          sideTitles: SideTitles(
            showTitles: true,
            reservedSize: 30,
            interval: 1,
            getTitlesWidget: (value, meta) {
              if (value.toInt() >= 0 && value.toInt() < dailyList.length) {
                final date = dailyList[value.toInt()].fxDate;
                final dateStr = date.split('-');
                return Padding(
                  padding: const EdgeInsets.only(top: 8),
                  child: Text(
                    '${dateStr[1]}/${dateStr[2]}',
                    style: TextStyle(
                      fontSize: 10,
                      color: Colors.grey.shade600,
                    ),
                  ),
                );
              }
              return const Text('');
            },
          ),
        ),
        leftTitles: AxisTitles(
          sideTitles: SideTitles(
            showTitles: true,
            reservedSize: 50,
            interval: maxY / 4,
            getTitlesWidget: (value, meta) {
              return Text(
                '${value.toInt()}mm',
                style: TextStyle(
                  fontSize: 10,
                  color: Colors.grey.shade600,
                ),
              );
            },
          ),
        ),
      ),
      borderData: FlBorderData(
        show: true,
        border: Border.all(color: Colors.grey.shade300),
      ),
      minX: 0,
      maxX: (dailyList.length - 1).toDouble(),
      minY: 0,
      maxY: maxY,
      barGroups: dailyList.asMap().entries.map((entry) {
        final precip = double.tryParse(entry.value.precip) ?? 0.0;
        return BarChartGroupData(
          x: entry.key,
          barRods: [
            BarChartRodData(
              toY: precip,
              color: Colors.blue,
              width: 16,
              borderRadius: const BorderRadius.vertical(
                top: Radius.circular(4),
              ),
            ),
          ],
        );
      }).toList(),
    ),
  );
}

image-20260130230645228

4.5 数据流程

📈 FlChart 📦 Daily数据 📊 WeatherTrendChart 📱 页面 📈 FlChart 📦 Daily数据 📊 WeatherTrendChart 📱 页面 alt [温度图表] [降水量图表] [湿度图表] 传入 dailyList 和 chartType 检查数据是否为空 提取温度/降水量/湿度数据 返回数值列表 创建最高温度 FlSpot 创建最低温度 FlSpot LineChart(最高温度线, 最低温度线) 创建降水量数据 BarChart(降水量柱状图) 创建湿度 FlSpot LineChart(湿度折线图) 渲染图表 返回 Widget 显示图表

5. 实际步骤

5.1 步骤1:添加依赖

pubspec.yaml 中添加:

dependencies:
  fl_chart: ^0.69.0

运行 flutter pub get 安装依赖。

5.2 步骤2:创建图表组件

创建 lib/widgets/weather_trend_chart.dart 文件,参考上面的代码实现。

💡 新手提示:

  • 使用 StatelessWidget 创建无状态组件
  • 通过构造函数接收数据和配置
  • 使用 switch 语句根据图表类型选择不同的构建方法

5.3 步骤3:在页面中集成图表

weather_detail_page.dart 中:

import '../widgets/weather_trend_chart.dart';

// 在页面底部添加图表
if (forecast.daily != null && forecast.daily!.isNotEmpty)
  WeatherTrendChart(
    dailyList: _getFilteredDailyList(forecast.daily!),
    chartType: ChartType.temperature,
  ),

5.4 步骤4:实现数据过滤方法

/// 获取过滤后的预报数据列表(根据选择的天数)
List<Daily> _getFilteredDailyList(List<Daily> dailyList) {
  final days = int.tryParse(_selectedDays.replaceAll('d', '')) ?? 7;
  return dailyList.take(days).toList();
}

功能说明:

  • 根据用户选择的预报天数(3、7、10、15、30)过滤数据
  • 只显示对应天数的预报数据

5.5 步骤5:测试图表

运行应用,检查:

  • ✅ 图表是否正确显示
  • ✅ 数据是否正确映射
  • ✅ 切换预报天数时图表是否更新
  • ✅ 图表样式是否符合预期

6. 常见错误与解决方案

6.1 错误:图表不显示

错误信息:

NoSuchMethodError: The method 'toDouble' was called on null.

可能原因:

  • 数据为空或格式不正确
  • 数据解析失败

解决方案:

// ✅ 正确:添加数据检查
if (dailyList.isEmpty) {
  return const SizedBox.shrink();
}

// ✅ 正确:使用安全的类型转换
final temp = double.tryParse(entry.value.tempMax) ?? 0.0;

6.2 错误:Y 轴范围不正确

可能原因:

  • Y 轴范围计算错误
  • 数据值超出范围

解决方案:

// ✅ 正确:动态计算 Y 轴范围
final maxTemp = dailyList.map((d) => double.tryParse(d.tempMax) ?? 0.0)
    .reduce((a, b) => a > b ? a : b);
final minTemp = dailyList.map((d) => double.tryParse(d.tempMin) ?? 0.0)
    .reduce((a, b) => a < b ? a : b);
final tempRange = maxTemp - minTemp;
final minY = (minTemp - tempRange * 0.1).floor().toDouble();
final maxY = (maxTemp + tempRange * 0.1).ceil().toDouble();

6.3 错误:X 轴标签不显示

可能原因:

  • reservedSize 设置太小
  • getTitlesWidget 返回空字符串

解决方案:

// ✅ 正确:设置合适的 reservedSize
bottomTitles: AxisTitles(
  sideTitles: SideTitles(
    showTitles: true,
    reservedSize: 30,  // 确保有足够空间
    interval: 1,
    getTitlesWidget: (value, meta) {
      if (value.toInt() >= 0 && value.toInt() < dailyList.length) {
        // 返回有效的标签文本
        return Text('标签');
      }
      return const Text('');
    },
  ),
),

6.4 错误:图表性能问题

可能原因:

  • 数据量过大
  • 图表更新过于频繁

解决方案:

// ✅ 正确:限制数据量
List<Daily> _getFilteredDailyList(List<Daily> dailyList) {
  final days = int.tryParse(_selectedDays.replaceAll('d', '')) ?? 7;
  return dailyList.take(days).toList();  // 只取需要的数据
}

// ✅ 正确:使用 const 构造函数
const SizedBox(height: 16),

6.5 错误:图表类型不匹配

可能原因:

  • ChartType 枚举未定义
  • 导入路径错误

解决方案:

// ✅ 正确:定义图表类型枚举
enum ChartType {
  temperature,   // 温度趋势
  precipitation, // 降水量趋势
  humidity,      // 湿度趋势
}

// ✅ 正确:导入图表组件
import '../widgets/weather_trend_chart.dart';

7. 进阶功能

7.1 多图表切换

class _WeatherTrendChartState extends State<WeatherTrendChart> {
  ChartType _currentChartType = ChartType.temperature;
  
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 图表类型选择器
        Row(
          children: [
            _buildChartTypeButton('温度', ChartType.temperature),
            _buildChartTypeButton('降水量', ChartType.precipitation),
            _buildChartTypeButton('湿度', ChartType.humidity),
          ],
        ),
        // 图表
        WeatherTrendChart(
          dailyList: widget.dailyList,
          chartType: _currentChartType,
        ),
      ],
    );
  }
  
  Widget _buildChartTypeButton(String label, ChartType type) {
    final isSelected = _currentChartType == type;
    return GestureDetector(
      onTap: () {
        setState(() {
          _currentChartType = type;
        });
      },
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        decoration: BoxDecoration(
          color: isSelected ? Colors.blue : Colors.grey.shade200,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Text(
          label,
          style: TextStyle(
            color: isSelected ? Colors.white : Colors.grey.shade700,
          ),
        ),
      ),
    );
  }
}

image-20260130230951467

7.2 图表交互

图表交互功能

点击数据点

长按显示详情

滑动查看数据

显示该日期的详细信息

弹出详情卡片

更新图表显示范围

7.3 图表动画

LineChart(
  LineChartData(
    // ... 其他配置
    lineTouchData: LineTouchData(
      touchTooltipData: LineTouchTooltipData(
        getTooltipItems: (List<LineBarSpot> touchedSpots) {
          return touchedSpots.map((LineBarSpot touchedSpot) {
            return LineTooltipItem(
              '${touchedSpot.y.toInt()}°',
              const TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            );
          }).toList();
        },
      ),
    ),
  ),
)

7.4 图表样式自定义

// 自定义颜色主题
class ChartTheme {
  static const Color primaryColor = Color(0xFF6366F1);
  static const Color secondaryColor = Color(0xFF8B5CF6);
  static const Color accentColor = Color(0xFFEC4899);
  
  static FlTitlesData getTitlesData() {
    return FlTitlesData(
      show: true,
      leftTitles: AxisTitles(
        sideTitles: SideTitles(
          showTitles: true,
          reservedSize: 50,
          getTitlesWidget: (value, meta) {
            return Text(
              '${value.toInt()}°',
              style: const TextStyle(
                color: Colors.grey,
                fontSize: 10,
              ),
            );
          },
        ),
      ),
      // ... 其他配置
    );
  }
}

8. 参考资料


9. 功能演示流程图

📈 FlChart 📦 预报数据 📊 WeatherTrendChart 📱 天气详情页 👤 用户 📈 FlChart 📦 预报数据 📊 WeatherTrendChart 📱 天气详情页 👤 用户 打开天气详情页 加载预报数据 获取 7 天预报数据 返回 Daily 列表 选择预报天数(如 15 天) _getFilteredDailyList(15天) 传入过滤后的数据 提取温度数据 创建 FlSpot 数据点 LineChart(最高温度, 最低温度) 渲染图表 返回 Widget 显示图表 用户查看温度趋势 切换为降水量图表 chartType = precipitation 提取降水量数据 BarChart(降水量) 返回 Widget 显示降水量图表 用户查看降水量趋势

🎉 祝你开发顺利! 🚀
欢迎加入开源鸿蒙跨平台社区

Logo

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

更多推荐