Flutter 使用 AI Cursor 快速完成一个图表封装【提效】

Flutter 使用 Cursor 快速完成图表封装(TWBarChartView)

前言

在现代移动应用开发中,数据可视化是不可或缺的功能。虽然市面上有很多成熟的图表库,但有时我们需要高度定制化的图表组件来满足特定的设计需求。本文记录了我使用 Cursor AI 编程助手快速完成一个 Flutter 自定义柱状图组件(TWBarChartView)的完整过程,包括遇到的问题和解决方案。

项目背景

在项目中,我们需要一个支持以下特性的柱状图组件:

  • 支持平均值水平线显示
  • 支持高亮特定柱子
  • 支持两种显示模式:平均值模式和滚动模式
  • 高度可定制的样式
  • 当平均值超出数据范围时能自动调整视图

原型

效果 demo

代码

dart 复制代码
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:dotted_line/dotted_line.dart';

enum TWBarChartType {
  average,
  scroll,
}

class TWBarChartView extends StatelessWidget {
  final double width;
  final double height;
  final List<double> data;
  final double averageValue;
  final int? highlightedIndex;
  final Color barColor;
  final Color highlightedBarColor;
  final Color averageLineColor;
  final Decoration? decoration;
  final String averageLabel;
  final EdgeInsets padding;
  final Widget? emptyWidget;
  final TWBarChartType type;

  /// 柱子宽度, 仅在type为 TWBarChartType.average 时有效
  final double barWidth;

  const TWBarChartView({
    super.key,
    required this.width,
    required this.height,
    required this.data,
    required this.averageValue,
    this.highlightedIndex,
    this.barColor = Colors.grey,
    this.highlightedBarColor = Colors.orange,
    this.averageLineColor = Colors.orange,
    this.decoration,
    this.averageLabel = '周边均价',
    this.padding = EdgeInsets.zero,
    this.emptyWidget,
    this.type = TWBarChartType.average,
    this.barWidth = 30,
  });

  double get containHeight => height - padding.vertical;

  @override
  Widget build(BuildContext context) {
    if (data.isEmpty) {
      return emptyWidget ??
          Container(
            width: width,
            height: height,
            alignment: Alignment.center,
            decoration: decoration ??
                BoxDecoration(
                  color: Colors.grey.shade200,
                  borderRadius: BorderRadius.circular(8),
                ),
            child: const Text('暂无数据'),
          );
    }

    // 将 averageValue 也纳入最大值和最小值的计算,确保平均值线能在视图范围内显示
    final dataMaxValue = data.reduce((a, b) => a > b ? a : b);
    final dataMinValue = data.reduce((a, b) => a < b ? a : b);
    final maxValue = max(dataMaxValue, averageValue);
    final minValue = min(dataMinValue, averageValue);
    final valueRange = maxValue - minValue;

    Widget resultWidget = Stack(
      alignment: Alignment.bottomLeft,
      children: [
        // 柱状图
        _buildChart(
          maxValue: maxValue,
          minValue: minValue,
          valueRange: valueRange,
        ),

        // 平均值水平线
        Positioned(
          bottom: _calculateAverageLinePosition(
            availableHeight: containHeight,
            value: averageValue,
            minValue: minValue,
            valueRange: valueRange,
          ),
          left: 0,
          right: 0,
          child: DottedLine(
            lineThickness: 1,
            dashColor: highlightedBarColor,
          ),
        ),
        // 平均值标签
        Positioned(
          bottom: _calculateAverageLinePosition(
            availableHeight: containHeight,
            value: averageValue,
            minValue: minValue,
            valueRange: valueRange,
          ),
          right: 10,
          child: Text(
            averageLabel,
            style: TextStyle(
              color: highlightedBarColor,
              fontSize: 12,
              fontWeight: FontWeight.w400,
            ),
          ),
        ),
      ],
    );

    resultWidget = Container(
      width: width,
      height: height,
      decoration: decoration ??
          BoxDecoration(
            color: Colors.grey.shade200,
            borderRadius: BorderRadius.circular(8),
          ),
      child: resultWidget,
    );

    return resultWidget;
  }

  /// 构建柱状图
  Widget _buildChart({
    required double maxValue,
    required double minValue,
    required double valueRange,
  }) {
    Widget resultWidget = switch (type) {
      TWBarChartType.average => _buildAverageChart(
          maxValue: maxValue,
          minValue: minValue,
          valueRange: valueRange,
        ),
      TWBarChartType.scroll => _buildScrollChart(
          maxValue: maxValue,
          minValue: minValue,
          valueRange: valueRange,
        ),
    };
    resultWidget = Padding(
      padding: padding,
      child: resultWidget,
    );

    return resultWidget;
  }

  /// 构建滚动柱状图
  Widget _buildScrollChart({
    required double maxValue,
    required double minValue,
    required double valueRange,
  }) {
    return ListView.separated(
      scrollDirection: Axis.horizontal,
      itemBuilder: (context, index) {
        return SizedBox(
          height: containHeight,
          child: Align(
            alignment: Alignment.bottomCenter,
            child: _buildBar(
              availableHeight: containHeight,
              value: data[index],
              index: index,
              maxValue: maxValue,
              minValue: minValue,
              valueRange: valueRange,
              type: type,
            ),
          ),
        );
      },
      separatorBuilder: (context, index) => const SizedBox(width: 10),
      itemCount: data.length,
    );
  }

  /// 构建平均值柱状图
  Widget _buildAverageChart({
    required double maxValue,
    required double minValue,
    required double valueRange,
  }) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      crossAxisAlignment: CrossAxisAlignment.end,
      children: List.generate(
        data.length,
        (index) => _buildBar(
          availableHeight: containHeight,
          value: data[index],
          index: index,
          maxValue: maxValue,
          minValue: minValue,
          valueRange: valueRange,
          type: type,
        ),
      ),
    );
  }

  /// 构建柱状图
  /// [value] 值
  /// [index] 索引
  /// [maxValue] 最大值
  /// [minValue] 最小值
  /// [valueRange] 值范围
  /// [availableHeight] 可用高度
  Widget _buildBar({
    required double availableHeight,
    required double value,
    required int index,
    required double maxValue,
    required double minValue,
    required double valueRange,
    required TWBarChartType type,
  }) {
    final isHighlighted = highlightedIndex == index;

    // 修复最小值显示问题:确保最小柱子也有最小高度,但允许值为0时显示为0
    double barHeight = _calculateAverageLinePosition(
      availableHeight: availableHeight,
      value: value,
      minValue: minValue,
      valueRange: valueRange,
    );

    return Container(
      width: type == TWBarChartType.average
          ? (width - padding.horizontal) / data.length * 0.6
          : barWidth, // 柱子宽度
      height: barHeight,
      decoration: BoxDecoration(
        color: isHighlighted ? highlightedBarColor : barColor,
        borderRadius: const BorderRadius.all(Radius.circular(4)),
      ),
    );
  }

  /// 计算平均值水平线位置
  /// [availableHeight] 可用高度
  /// [value] 值
  /// [minValue] 最小值
  /// [valueRange] 值范围
  double _calculateAverageLinePosition({
    required double availableHeight,
    required double value,
    required double minValue,
    required double valueRange,
  }) {
    // 修复最小值显示问题:确保最小柱子也有最小高度,但允许值为0时显示为0
    double barHeight = 0.0;
    if (valueRange > 0.0) {
      final normalizedValue = (value - minValue) / valueRange;
      barHeight = normalizedValue * availableHeight * 0.8;
      // 只有当值不为0且高度小于1像素时,才设置最小高度
      if (value >= 0 && barHeight < 1) {
        barHeight = 1;
      }
    }
    return barHeight;
  }
}

开发过程记录

第一阶段:基础组件结构

最初的 TWBarChartView 组件已经具备了基本的柱状图功能,但在使用过程中发现了一些问题。

dart 复制代码
enum TWBarChartType {
  average,  // 平均值模式:所有柱子在一行显示
  scroll,   // 滚动模式:可横向滚动查看
}

class TWBarChartView extends StatelessWidget {
  final double width;
  final double height;
  final List<double> data;
  final double averageValue;
  final TWBarChartType type;
  // ... 其他属性
}

第二阶段:解决滚动模式布局问题

问题发现

在使用 TWBarChartType.scroll 类型时,发现了一个关键问题:

用户反馈: "为什么 _buildScrollChart 滚动的高度占满整个视图?"

问题分析

使用 Cursor 分析代码后,发现问题出现在 _buildScrollChart 方法中:

dart 复制代码
// 原始问题代码
Widget _buildScrollChart({...}) {
  return ListView.separated(
    scrollDirection: Axis.horizontal,
    itemBuilder: (context, index) {
      return _buildBar(...); // 直接返回柱子组件
    },
    // ...
  );
}

问题根源: ListView 在垂直方向上会强制子组件占满可用空间,导致所有柱子高度相同。

解决方案

通过 Cursor 的建议,我们采用了以下解决方案:

dart 复制代码
Widget _buildScrollChart({...}) {
  return ListView.separated(
    scrollDirection: Axis.horizontal,
    itemBuilder: (context, index) {
      return SizedBox(
        height: containHeight,
        child: Align(
          alignment: Alignment.bottomCenter,
          child: _buildBar(...),
        ),
      );
    },
    // ...
  );
}

关键改进:

  1. SizedBox 容器:提供明确的高度约束
  2. Align 组件:确保柱子对齐到底部,不会被强制拉伸
  3. 保持计算高度:柱子可以保持根据数据计算的真实高度
技术原理深入

Cursor 帮我分析了 Flutter 布局约束的传递机制:

scss 复制代码
Container(height: height)
  └── Stack
      └── ListView (scrollDirection: Axis.horizontal)
          └── SizedBox(height: containHeight)  // 固定高度容器
              └── Align(alignment: Alignment.bottomCenter)  // 不强制拉伸
                  └── _buildBar (height: barHeight)  // 保持计算高度

对比平均值模式的实现:

dart 复制代码
Row(
  crossAxisAlignment: CrossAxisAlignment.end,  // 底部对齐,不强制拉伸
  children: List.generate(data.length, (index) => _buildBar(...)),
)

第三阶段:优化平均值线显示范围

问题发现

用户反馈: "考虑如果 averageValue 值比最大值还大很多,比如 168,此时就无法展示平均值线在视图里"

在测试中发现,当平均值(158)远大于数据最大值(138)时,平均值线会超出视图范围。

原始计算逻辑
dart 复制代码
final maxValue = data.reduce((a, b) => a > b ? a : b);  // 只考虑数据
final minValue = data.reduce((a, b) => a < b ? a : b);  // 只考虑数据
优化解决方案

通过 Cursor 的协助,我们重新设计了计算逻辑:

dart 复制代码
// 将 averageValue 也纳入最大值和最小值的计算
final dataMaxValue = data.reduce((a, b) => a > b ? a : b);
final dataMinValue = data.reduce((a, b) => a < b ? a : b);
final maxValue = max(dataMaxValue, averageValue);
final minValue = min(dataMinValue, averageValue);
final valueRange = maxValue - minValue;

改进效果:

  • 视图范围从 33-138 扩展到 33-158
  • 平均值线现在显示在视图顶部可见位置
  • 所有柱子高度按新范围重新计算,保持正确比例

第四阶段:增强组件可定制性

添加柱子宽度配置

根据实际使用需求,增加了柱子宽度的可配置性:

dart 复制代码
class TWBarChartView extends StatelessWidget {
  /// 柱子宽度, 仅在type为 TWBarChartType.average 时有效
  final double barWidth;
  
  const TWBarChartView({
    // ...
    this.barWidth = 30,
  });
  
  // 在 _buildBar 中使用
  Container(
    width: type == TWBarChartType.average
        ? (width - padding.horizontal) / data.length * 0.6
        : barWidth, // 使用可配置的柱子宽度
    // ...
  )
}

Cursor AI 在开发过程中的作用

1. 问题诊断专家

当遇到布局问题时,Cursor 能够:

  • 快速定位问题根源(ListView 约束机制)
  • 分析 Flutter 布局原理
  • 提供详细的技术解释

2. 代码重构助手

  • 建议最佳实践的解决方案
  • 保持代码结构清晰
  • 确保修改不破坏现有功能

3. 技术文档生成

  • 自动生成详细的代码注释
  • 解释复杂的布局逻辑
  • 提供使用示例

最终实现效果

核心特性

  1. 双模式支持

    • TWBarChartType.average:所有柱子在一行显示,适合数据较少的场景
    • TWBarChartType.scroll:横向滚动查看,适合大量数据展示
  2. 智能视图范围

    • 自动将平均值纳入计算范围
    • 确保平均值线始终可见
    • 保持数据比例的准确性
  3. 高度可定制

    • 支持自定义颜色、尺寸、间距
    • 可配置柱子宽度
    • 支持高亮显示特定柱子

使用示例

dart 复制代码
TWBarChartView(
  width: 350.w,
  height: 200.w,
  data: const [120.0, 78.0, 72.0, 45.0, 138.0, 100.0, 35.0],
  averageValue: 158,  // 即使大于数据最大值也能正确显示
  type: TWBarChartType.scroll,
  highlightedIndex: 3,
  barColor: Colors.grey,
  barWidth: 25,
  padding: EdgeInsets.symmetric(horizontal: 10.w),
)

开发心得与总结

1. AI 辅助编程的优势

  • 快速问题定位:Cursor 能在几秒内分析出布局问题的根本原因
  • 最佳实践建议:提供符合 Flutter 设计理念的解决方案
  • 代码质量提升:自动优化代码结构,添加必要注释

2. Flutter 布局的关键认知

通过这次开发,深入理解了:

  • ListView 在不同滚动方向下的约束行为
  • Align 组件在布局中的重要作用
  • Container 高度约束的优先级机制

3. 组件设计的思考

  • 渐进式增强:从基础功能开始,逐步添加高级特性
  • 边界情况处理:考虑平均值超出数据范围等特殊场景
  • API 设计:保持接口简洁,同时提供足够的定制能力

性能与优化建议

  1. 数据处理优化
dart 复制代码
// 使用 dart:math 库的 min/max 函数替代 reduce
final maxValue = max(dataMaxValue, averageValue);
final minValue = min(dataMinValue, averageValue);
  1. 渲染优化
  • 在滚动模式下,ListView 自带视图复用机制
  • 避免在 build 方法中进行复杂计算
  1. 内存管理
  • 对于大量数据,考虑实现懒加载
  • 合理设置 ListView 的 cacheExtent

未来优化方向

  1. 动画支持:添加柱子高度变化动画
  2. 交互增强:支持点击、长按等手势操作
  3. 数据绑定:支持数据流的实时更新
  4. 主题适配:更好地集成 Flutter 主题系统

结语

1、 通过使用 Cursor AI 辅助开发,我们在短时间内完成了一个功能完整、设计精良的图表组件。这个过程不仅提高了开发效率,更重要的是通过 AI 的分析和建议,深入理解了 Flutter 布局机制的底层原理。

2、 AI 辅助编程不是替代程序员的思考,而是增强我们解决问题的能力。在复杂的技术问题面前,AI 可以提供快速的分析和多种解决方案,让我们能够专注于更高层次的架构设计和用户体验优化。

3、 这次的开发经历证明,合理利用 AI 工具可以显著提升开发质量和效率,是现代软件开发不可或缺的助力。

文章我也是通过 AI 生成,我在中间除了修改了下计算逻辑,大部分时间是在调试对接和自测。

相关推荐
何双新2 小时前
Odoo AI 智能查询系统
前端·人工智能·python
HH思️️无邪5 小时前
Flutter 开发技巧 AI 快速构建 json_annotation model 的提示词
flutter·json
秋名山大前端8 小时前
Chrome GPU 加速优化配置(前端 3D 可视化 / 数字孪生专用)
前端·chrome·3d
今天不要写bug8 小时前
antv x6实现封装拖拽流程图配置(适用于工单流程、审批流程应用场景)
前端·typescript·vue·流程图
luquinn8 小时前
实现统一门户登录跳转免登录
开发语言·前端·javascript
用户21411832636029 小时前
dify案例分享-5分钟搭建智能思维导图系统!Dify + MCP工具实战教程
前端
augenstern4169 小时前
HTML(面试)
前端
excel9 小时前
前端常见布局误区:1fr 为什么撑爆了我的容器?
前端
烛阴9 小时前
TypeScript 类型魔法:像遍历对象一样改造你的类型
前端·javascript·typescript
vayy9 小时前
uniapp中 ios端 scroll-view 组件内部子元素z-index失效问题
前端·ios·微信小程序·uni-app