Flutter 框架跨平台鸿蒙开发 - 实时蔬菜价格查询:智能掌握菜价动态

Flutter实时蔬菜价格查询:智能掌握菜价动态

项目简介

实时蔬菜价格查询是一款专为居民日常买菜打造的Flutter应用,提供多城市蔬菜价格实时查询、分类统计和价格趋势分析功能。通过智能搜索和数据可视化,让用户轻松掌握菜价动态,合理安排采购计划。
运行效果图



核心功能

  • 多城市查询:支持15个主要城市价格查询
  • 蔬菜品种:32种常见蔬菜,涵盖6大分类
  • 实时价格:显示当前价格和昨日对比
  • 价格变化:自动计算涨跌幅度和百分比
  • 市场信息:显示批发市场来源
  • 智能搜索:支持蔬菜名称快速搜索
  • 分类筛选:按叶菜类、根茎类等分类查看
  • 价格趋势:统计上涨、下跌、持平数量
  • 区间分布:可视化展示价格区间分布
  • 详情页面:7日价格走势图表和营养信息
  • 数据刷新:一键更新最新价格数据

技术特点

  • Material Design 3设计风格
  • NavigationBar底部导航
  • 三页面架构(价格列表、分类统计、价格趋势)
  • CustomPainter自定义图表绘制
  • 计算属性实现动态数据
  • 响应式卡片布局
  • 模拟数据生成
  • 无需额外依赖包

核心代码实现

1. 蔬菜数据模型

dart 复制代码
class Vegetable {
  final String id;              // 蔬菜ID
  final String name;            // 蔬菜名称
  final String category;        // 分类
  final String unit;            // 单位
  final String image;           // 图标(Emoji)
  double price;                 // 当前价格
  double yesterdayPrice;        // 昨日价格
  String market;                // 市场名称
  DateTime updateTime;          // 更新时间

  Vegetable({
    required this.id,
    required this.name,
    required this.category,
    required this.unit,
    required this.image,
    required this.price,
    required this.yesterdayPrice,
    required this.market,
    required this.updateTime,
  });

  // 价格变化量
  double get priceChange => price - yesterdayPrice;
  
  // 价格变化百分比
  double get priceChangePercent => (priceChange / yesterdayPrice) * 100;
  
  // 是否涨价
  bool get isPriceUp => priceChange > 0;
  
  // 是否降价
  bool get isPriceDown => priceChange < 0;

  // 分类颜色
  Color get categoryColor {
    switch (category) {
      case '叶菜类': return Colors.green;
      case '根茎类': return Colors.brown;
      case '瓜果类': return Colors.orange;
      case '豆类': return Colors.purple;
      case '菌菇类': return Colors.grey;
      case '其他': return Colors.blue;
      default: return Colors.grey;
    }
  }

  // 分类图标
  IconData get categoryIcon {
    switch (category) {
      case '叶菜类': return Icons.eco;
      case '根茎类': return Icons.grass;
      case '瓜果类': return Icons.apple;
      case '豆类': return Icons.grain;
      case '菌菇类': return Icons.park;
      case '其他': return Icons.restaurant;
      default: return Icons.shopping_basket;
    }
  }
}

模型字段说明

字段 类型 说明
id String 唯一标识符
name String 蔬菜名称
category String 分类(6大类)
unit String 计量单位
image String Emoji图标
price double 当前价格(元)
yesterdayPrice double 昨日价格(元)
market String 批发市场名称
updateTime DateTime 数据更新时间

计算属性

  • priceChange:价格变化量(当前价格 - 昨日价格)
  • priceChangePercent:价格变化百分比
  • isPriceUp:是否涨价(价格变化量 > 0)
  • isPriceDown:是否降价(价格变化量 < 0)
  • categoryColor:根据分类返回对应颜色
  • categoryIcon:根据分类返回对应图标

蔬菜分类与颜色映射

分类 颜色 图标 说明
叶菜类 绿色 eco 白菜、菠菜、生菜等
根茎类 棕色 grass 土豆、萝卜、山药等
瓜果类 橙色 apple 番茄、黄瓜、茄子等
豆类 紫色 grain 豆角、豌豆、毛豆等
菌菇类 灰色 park 香菇、平菇、金针菇等
其他 蓝色 restaurant 大葱、洋葱、大蒜等

2. 数据生成逻辑

dart 复制代码
void _generateVegetableData() {
  final random = Random();

  // 32种蔬菜数据
  final vegetableList = [
    {'name': '白菜', 'category': '叶菜类', 'unit': '斤', 'image': '🥬'},
    {'name': '菠菜', 'category': '叶菜类', 'unit': '斤', 'image': '🥬'},
    {'name': '生菜', 'category': '叶菜类', 'unit': '斤', 'image': '🥬'},
    {'name': '油菜', 'category': '叶菜类', 'unit': '斤', 'image': '🥬'},
    {'name': '芹菜', 'category': '叶菜类', 'unit': '斤', 'image': '🥬'},
    {'name': '韭菜', 'category': '叶菜类', 'unit': '斤', 'image': '🥬'},
    {'name': '香菜', 'category': '叶菜类', 'unit': '斤', 'image': '🥬'},
    {'name': '土豆', 'category': '根茎类', 'unit': '斤', 'image': '🥔'},
    {'name': '红薯', 'category': '根茎类', 'unit': '斤', 'image': '🍠'},
    {'name': '萝卜', 'category': '根茎类', 'unit': '斤', 'image': '🥕'},
    {'name': '胡萝卜', 'category': '根茎类', 'unit': '斤', 'image': '🥕'},
    {'name': '山药', 'category': '根茎类', 'unit': '斤', 'image': '🥔'},
    {'name': '莲藕', 'category': '根茎类', 'unit': '斤', 'image': '🥔'},
    {'name': '番茄', 'category': '瓜果类', 'unit': '斤', 'image': '🍅'},
    {'name': '黄瓜', 'category': '瓜果类', 'unit': '斤', 'image': '🥒'},
    {'name': '茄子', 'category': '瓜果类', 'unit': '斤', 'image': '🍆'},
    {'name': '辣椒', 'category': '瓜果类', 'unit': '斤', 'image': '🌶️'},
    {'name': '青椒', 'category': '瓜果类', 'unit': '斤', 'image': '🫑'},
    {'name': '南瓜', 'category': '瓜果类', 'unit': '斤', 'image': '🎃'},
    {'name': '冬瓜', 'category': '瓜果类', 'unit': '斤', 'image': '🥒'},
    {'name': '豆角', 'category': '豆类', 'unit': '斤', 'image': '🫘'},
    {'name': '豌豆', 'category': '豆类', 'unit': '斤', 'image': '🫘'},
    {'name': '毛豆', 'category': '豆类', 'unit': '斤', 'image': '🫘'},
    {'name': '蚕豆', 'category': '豆类', 'unit': '斤', 'image': '🫘'},
    {'name': '香菇', 'category': '菌菇类', 'unit': '斤', 'image': '🍄'},
    {'name': '平菇', 'category': '菌菇类', 'unit': '斤', 'image': '🍄'},
    {'name': '金针菇', 'category': '菌菇类', 'unit': '斤', 'image': '🍄'},
    {'name': '木耳', 'category': '菌菇类', 'unit': '斤', 'image': '🍄'},
    {'name': '大葱', 'category': '其他', 'unit': '斤', 'image': '🧅'},
    {'name': '洋葱', 'category': '其他', 'unit': '斤', 'image': '🧅'},
    {'name': '大蒜', 'category': '其他', 'unit': '斤', 'image': '🧄'},
    {'name': '生姜', 'category': '其他', 'unit': '斤', 'image': '🫚'},
  ];

  // 生成蔬菜数据
  _allVegetables = vegetableList.map((veg) {
    final basePrice = 2.0 + random.nextDouble() * 8.0;  // 2-10元
    final yesterdayPrice = basePrice + (random.nextDouble() - 0.5) * 2.0;

    return Vegetable(
      id: 'veg_${vegetableList.indexOf(veg)}',
      name: veg['name'] as String,
      category: veg['category'] as String,
      unit: veg['unit'] as String,
      image: veg['image'] as String,
      price: double.parse(basePrice.toStringAsFixed(2)),
      yesterdayPrice: double.parse(yesterdayPrice.toStringAsFixed(2)),
      market: _markets[random.nextInt(_markets.length)],
      updateTime: DateTime.now().subtract(
        Duration(minutes: random.nextInt(120))  // 0-120分钟前
      ),
    );
  }).toList();

  _applyFilters();
}

数据生成特点

  1. 32种常见蔬菜,涵盖6大分类
  2. 价格范围:2-10元/斤
  3. 昨日价格:当前价格±1元随机波动
  4. 随机分配5个批发市场
  5. 更新时间:0-120分钟前随机
  6. 价格保留2位小数

批发市场列表

  • 新发地农产品批发市场
  • 岳各庄批发市场
  • 八里桥批发市场
  • 大洋路农副产品批发市场
  • 锦绣大地批发市场

3. 搜索和筛选功能

dart 复制代码
void _applyFilters() {
  setState(() {
    _filteredVegetables = _allVegetables.where((veg) {
      // 搜索关键词筛选
      if (_searchQuery.isNotEmpty) {
        if (!veg.name.toLowerCase().contains(_searchQuery.toLowerCase())) {
          return false;
        }
      }
      
      // 分类筛选
      if (_selectedCategory != '全部' && veg.category != _selectedCategory) {
        return false;
      }
      
      return true;
    }).toList();
  });
}

筛选条件

筛选项 说明 实现方式
搜索关键词 匹配蔬菜名称 不区分大小写的contains匹配
分类筛选 按6大分类筛选 精确匹配category字段

筛选流程

  1. 检查搜索关键词是否匹配蔬菜名称
  2. 检查分类是否匹配("全部"跳过此检查)
  3. 更新筛选结果列表
  4. 触发UI重新渲染

4. 价格刷新功能

dart 复制代码
void _refreshPrices() {
  final random = Random();
  setState(() {
    for (var veg in _allVegetables) {
      veg.yesterdayPrice = veg.price;  // 当前价格变为昨日价格
      veg.price = double.parse(
        (veg.price + (random.nextDouble() - 0.5) * 1.0).toStringAsFixed(2)
      );
      if (veg.price < 0.5) veg.price = 0.5;  // 最低价格0.5元
      veg.updateTime = DateTime.now();  // 更新时间
    }
    _applyFilters();
  });
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('价格已更新')),
  );
}

刷新逻辑

  1. 将当前价格保存为昨日价格
  2. 在当前价格基础上±0.5元随机波动
  3. 确保价格不低于0.5元
  4. 更新时间戳为当前时间
  5. 重新应用筛选条件
  6. 显示刷新成功提示
dart 复制代码
bottomNavigationBar: NavigationBar(
  selectedIndex: _selectedIndex,
  onDestinationSelected: (index) {
    setState(() => _selectedIndex = index);
  },
  destinations: const [
    NavigationDestination(icon: Icon(Icons.list), label: '价格列表'),
    NavigationDestination(icon: Icon(Icons.category), label: '分类'),
    NavigationDestination(icon: Icon(Icons.trending_up), label: '价格趋势'),
  ],
),

三个页面

页面 图标 功能
价格列表 list 显示所有蔬菜价格卡片
分类 category 按分类统计蔬菜数量和平均价格
价格趋势 trending_up 显示价格涨跌统计和区间分布

IndexedStack使用

dart 复制代码
Expanded(
  child: IndexedStack(
    index: _selectedIndex,
    children: [
      _buildPriceListPage(),
      _buildCategoryPage(),
      _buildTrendPage(),
    ],
  ),
),

IndexedStack的优势:

  • 保持所有页面状态
  • 切换时不重新构建
  • 提升用户体验

6. 城市选择器

dart 复制代码
Widget _buildCitySelector() {
  return Container(
    padding: const EdgeInsets.all(16),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text('选择城市',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
        const SizedBox(height: 16),
        Expanded(
          child: GridView.builder(
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3,        // 3列
              childAspectRatio: 2.5,    // 宽高比
              crossAxisSpacing: 8,      // 横向间距
              mainAxisSpacing: 8,       // 纵向间距
            ),
            itemCount: _cities.length,
            itemBuilder: (context, index) {
              final city = _cities[index];
              final isSelected = city == _selectedCity;
              return InkWell(
                onTap: () {
                  setState(() {
                    _selectedCity = city;
                    _generateVegetableData();
                  });
                  Navigator.pop(context);
                },
                child: Container(
                  decoration: BoxDecoration(
                    color: isSelected ? Colors.green : Colors.grey[200],
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Center(
                    child: Text(
                      city,
                      style: TextStyle(
                        color: isSelected ? Colors.white : Colors.black,
                        fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
                      ),
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    ),
  );
}

支持城市(15个):

  • 北京市、上海市、广州市、深圳市
  • 成都市、杭州市、武汉市、西安市
  • 南京市、重庆市、天津市、苏州市
  • 郑州市、长沙市、沈阳市

设计特点

  • 使用ModalBottomSheet展示
  • GridView网格布局,3列显示
  • 选中城市高亮显示(绿色背景)
  • 点击后自动关闭并刷新数据

7. ChoiceChip分类筛选

dart 复制代码
Widget _buildCategoryTabs() {
  return Container(
    height: 50,
    padding: const EdgeInsets.symmetric(horizontal: 16),
    child: ListView.builder(
      scrollDirection: Axis.horizontal,
      itemCount: _categories.length,
      itemBuilder: (context, index) {
        final category = _categories[index];
        final isSelected = category == _selectedCategory;
        return Padding(
          padding: const EdgeInsets.only(right: 8),
          child: ChoiceChip(
            label: Text(category),
            selected: isSelected,
            onSelected: (selected) {
              setState(() {
                _selectedCategory = category;
                _applyFilters();
              });
            },
          ),
        );
      },
    ),
  );
}

ChoiceChip特点

  • Material Design 3组件
  • 自动处理选中状态样式
  • 支持单选和多选模式
  • 适合标签筛选场景

分类列表

  • 全部、叶菜类、根茎类、瓜果类、豆类、菌菇类、其他

8. 蔬菜价格卡片

dart 复制代码
Widget _buildVegetableCard(Vegetable veg) {
  return Card(
    margin: const EdgeInsets.only(bottom: 12),
    child: InkWell(
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (_) => VegetableDetailPage(
              vegetable: veg,
              city: _selectedCity
            ),
          ),
        );
      },
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            // 左侧:蔬菜图标
            Container(
              width: 60,
              height: 60,
              decoration: BoxDecoration(
                color: veg.categoryColor.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(12),
              ),
              child: Center(
                child: Text(veg.image, style: const TextStyle(fontSize: 32)),
              ),
            ),
            const SizedBox(width: 16),
            // 中间:蔬菜信息
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Text(veg.name, style: const TextStyle(
                        fontSize: 18, fontWeight: FontWeight.bold)),
                      const SizedBox(width: 8),
                      Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 6, vertical: 2),
                        decoration: BoxDecoration(
                          color: veg.categoryColor.withValues(alpha: 0.2),
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: Text(veg.category,
                          style: TextStyle(fontSize: 10, color: veg.categoryColor)),
                      ),
                    ],
                  ),
                  const SizedBox(height: 4),
                  Text(veg.market,
                    style: const TextStyle(fontSize: 12, color: Colors.grey),
                    maxLines: 1, overflow: TextOverflow.ellipsis),
                  const SizedBox(height: 4),
                  Text('更新时间:${_formatTime(veg.updateTime)}',
                    style: const TextStyle(fontSize: 10, color: Colors.grey)),
                ],
              ),
            ),
            // 右侧:价格信息
            Column(
              crossAxisAlignment: CrossAxisAlignment.end,
              children: [
                Row(
                  children: [
                    Text('¥${veg.price.toStringAsFixed(2)}',
                      style: const TextStyle(fontSize: 24,
                        fontWeight: FontWeight.bold, color: Colors.green)),
                    Text('/${veg.unit}',
                      style: const TextStyle(fontSize: 12, color: Colors.grey)),
                  ],
                ),
                const SizedBox(height: 4),
                if (veg.priceChange != 0)
                  Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 6, vertical: 2),
                    decoration: BoxDecoration(
                      color: veg.isPriceUp
                        ? Colors.red.withValues(alpha: 0.1)
                        : Colors.green.withValues(alpha: 0.1),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Icon(veg.isPriceUp ? Icons.arrow_upward : Icons.arrow_downward,
                          size: 12, color: veg.isPriceUp ? Colors.red : Colors.green),
                        Text('${veg.priceChangePercent.abs().toStringAsFixed(1)}%',
                          style: TextStyle(fontSize: 12,
                            color: veg.isPriceUp ? Colors.red : Colors.green)),
                      ],
                    ),
                  ),
              ],
            ),
          ],
        ),
      ),
    ),
  );
}

卡片布局结构

  1. 左侧:蔬菜Emoji图标(60x60,带分类颜色背景)
  2. 中间:蔬菜名称、分类标签、市场名称、更新时间
  3. 右侧:当前价格、涨跌幅度标签

价格变化显示

  • 涨价:红色背景,向上箭头,红色文字
  • 降价:绿色背景,向下箭头,绿色文字
  • 持平:不显示变化标签

9. 分类统计页面

dart 复制代码
Widget _buildCategoryPage() {
  final categoryStats = <String, Map<String, dynamic>>{};
  
  // 统计各分类数据
  for (var category in _categories.skip(1)) {  // 跳过"全部"
    final vegs = _allVegetables.where((v) => v.category == category).toList();
    if (vegs.isNotEmpty) {
      final avgPrice = vegs.map((v) => v.price).reduce((a, b) => a + b) / vegs.length;
      categoryStats[category] = {
        'count': vegs.length,
        'avgPrice': avgPrice,
        'vegs': vegs,
      };
    }
  }

  return ListView(
    padding: const EdgeInsets.all(16),
    children: categoryStats.entries.map((entry) {
      final category = entry.key;
      final stats = entry.value;
      final veg = (stats['vegs'] as List<Vegetable>).first;

      return Card(
        margin: const EdgeInsets.only(bottom: 16),
        child: InkWell(
          onTap: () {
            setState(() {
              _selectedIndex = 0;  // 切换到价格列表页
              _selectedCategory = category;
              _applyFilters();
            });
          },
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Container(
                  padding: const EdgeInsets.all(16),
                  decoration: BoxDecoration(
                    color: veg.categoryColor.withValues(alpha: 0.1),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Icon(veg.categoryIcon,
                    color: veg.categoryColor, size: 32),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(category, style: const TextStyle(
                        fontSize: 18, fontWeight: FontWeight.bold)),
                      const SizedBox(height: 4),
                      Text('${stats['count']} 种蔬菜',
                        style: const TextStyle(fontSize: 14, color: Colors.grey)),
                      const SizedBox(height: 4),
                      Text('平均价格:¥${(stats['avgPrice'] as double).toStringAsFixed(2)}/斤',
                        style: const TextStyle(fontSize: 14, color: Colors.green)),
                    ],
                  ),
                ),
                const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
              ],
            ),
          ),
        ),
      );
    }).toList(),
  );
}

统计逻辑

  1. 遍历所有分类(跳过"全部")
  2. 筛选出每个分类的蔬菜列表
  3. 计算蔬菜数量
  4. 计算平均价格(总价格/数量)
  5. 存储统计数据

点击交互

  • 点击分类卡片
  • 切换到价格列表页
  • 自动应用该分类筛选
  • 显示该分类的所有蔬菜

10. 价格趋势页面

dart 复制代码
Widget _buildTrendPage() {
  // 价格区间统计
  final priceRanges = {
    '0-2元': _allVegetables.where((v) => v.price < 2).length,
    '2-4元': _allVegetables.where((v) => v.price >= 2 && v.price < 4).length,
    '4-6元': _allVegetables.where((v) => v.price >= 4 && v.price < 6).length,
    '6-8元': _allVegetables.where((v) => v.price >= 6 && v.price < 8).length,
    '8元以上': _allVegetables.where((v) => v.price >= 8).length,
  };

  // 涨跌统计
  final priceUpCount = _allVegetables.where((v) => v.isPriceUp).length;
  final priceDownCount = _allVegetables.where((v) => v.isPriceDown).length;
  final priceStableCount = _allVegetables.length - priceUpCount - priceDownCount;

  return ListView(
    padding: const EdgeInsets.all(16),
    children: [
      // 价格趋势统计卡片
      Card(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Row(
                children: [
                  Icon(Icons.trending_up, color: Colors.green),
                  SizedBox(width: 8),
                  Text('价格趋势统计',
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                ],
              ),
              const SizedBox(height: 16),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  _buildTrendItem('上涨', priceUpCount, Colors.red, Icons.arrow_upward),
                  _buildTrendItem('下跌', priceDownCount, Colors.green, Icons.arrow_downward),
                  _buildTrendItem('持平', priceStableCount, Colors.grey, Icons.remove),
                ],
              ),
            ],
          ),
        ),
      ),
      const SizedBox(height: 16),
      // 价格区间分布卡片
      Card(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Row(
                children: [
                  Icon(Icons.bar_chart, color: Colors.green),
                  SizedBox(width: 8),
                  Text('价格区间分布',
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                ],
              ),
              const SizedBox(height: 16),
              ...priceRanges.entries.map((entry) {
                final percentage = (entry.value / _allVegetables.length * 100);
                return Padding(
                  padding: const EdgeInsets.only(bottom: 12),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Text(entry.key, style: const TextStyle(fontSize: 14)),
                          Text('${entry.value} 种',
                            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
                        ],
                      ),
                      const SizedBox(height: 4),
                      LinearProgressIndicator(
                        value: percentage / 100,
                        backgroundColor: Colors.grey[200],
                        color: Colors.green,
                      ),
                      const SizedBox(height: 2),
                      Text('占比 ${percentage.toStringAsFixed(1)}%',
                        style: const TextStyle(fontSize: 12, color: Colors.grey)),
                    ],
                  ),
                );
              }),
            ],
          ),
        ),
      ),
    ],
  );
}

Widget _buildTrendItem(String label, int count, Color color, IconData icon) {
  return Column(
    children: [
      Container(
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: color.withValues(alpha: 0.1),
          borderRadius: BorderRadius.circular(12),
        ),
        child: Icon(icon, color: color, size: 32),
      ),
      const SizedBox(height: 8),
      Text(count.toString(),
        style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: color)),
      Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
    ],
  );
}

趋势统计

  • 上涨:价格变化量 > 0,红色显示
  • 下跌:价格变化量 < 0,绿色显示
  • 持平:价格变化量 = 0,灰色显示

区间分布

  • 0-2元、2-4元、4-6元、6-8元、8元以上
  • 使用LinearProgressIndicator可视化占比
  • 显示具体数量和百分比

11. 蔬菜详情页

dart 复制代码
class VegetableDetailPage extends StatelessWidget {
  final Vegetable vegetable;
  final String city;

  const VegetableDetailPage({
    super.key,
    required this.vegetable,
    required this.city,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(vegetable.name),
        actions: [
          IconButton(
            onPressed: () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('已添加到收藏')),
              );
            },
            icon: const Icon(Icons.favorite_border),
          ),
          IconButton(
            onPressed: () {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('分享功能')),
              );
            },
            icon: const Icon(Icons.share),
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildHeader(),          // 头部展示
            _buildPriceInfo(),       // 价格信息
            _buildMarketInfo(),      // 市场信息
            _buildPriceHistory(),    // 价格走势
            _buildNutritionInfo(),   // 营养价值
          ],
        ),
      ),
    );
  }
}

详情页结构

  1. 头部:蔬菜图标、名称、分类标签
  2. 价格信息:当前价格、昨日价格、价格变化、涨跌幅度
  3. 市场信息:城市、市场名称、更新时间、数据来源
  4. 价格走势:近7日价格曲线图和数据列表
  5. 营养价值:蔬菜营养成分和健康功效

12. CustomPainter价格曲线

dart 复制代码
class PriceChartPainter extends CustomPainter {
  final List<Map<String, dynamic>> history;

  PriceChartPainter(this.history);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2
      ..color = Colors.green;

    // 获取价格范围
    final prices = history.map((h) => h['price'] as double).toList();
    final maxPrice = prices.reduce((a, b) => a > b ? a : b);
    final minPrice = prices.reduce((a, b) => a < b ? a : b);
    final priceRange = maxPrice - minPrice;

    // 绘制价格曲线
    final path = Path();
    for (int i = 0; i < history.length; i++) {
      final x = (size.width / (history.length - 1)) * i;
      final price = prices[i];
      final y = size.height - ((price - minPrice) / priceRange) * size.height;

      if (i == 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }

      // 绘制数据点
      canvas.drawCircle(Offset(x, y), 4, paint..style = PaintingStyle.fill);
      paint.style = PaintingStyle.stroke;
    }

    canvas.drawPath(path, paint);

    // 绘制网格线
    final gridPaint = Paint()
      ..color = Colors.grey.withValues(alpha: 0.2)
      ..strokeWidth = 1;

    for (int i = 0; i <= 4; i++) {
      final y = (size.height / 4) * i;
      canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

绘制逻辑

  1. 计算价格最大值和最小值
  2. 计算价格范围(maxPrice - minPrice)
  3. 将价格映射到画布坐标
  4. 使用Path连接各个数据点
  5. 绘制圆点标记数据点
  6. 绘制网格线辅助阅读

坐标转换公式

复制代码
x = (画布宽度 / (数据点数 - 1)) * 索引
y = 画布高度 - ((价格 - 最小价格) / 价格范围) * 画布高度

注意:Canvas的Y轴向下为正,需要用画布高度减去计算值实现翻转。

13. 时间格式化

dart 复制代码
String _formatTime(DateTime time) {
  final now = DateTime.now();
  final diff = now.difference(time);

  if (diff.inMinutes < 1) return '刚刚';
  if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前';
  if (diff.inHours < 24) return '${diff.inHours}小时前';
  return '${diff.inDays}天前';
}

String _formatDateTime(DateTime time) {
  return '${time.year}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} '
      '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
}

两种格式化方式

方法 用途 示例
_formatTime 相对时间 "刚刚"、"5分钟前"、"2小时前"
_formatDateTime 绝对时间 "2024-01-22 14:30"

相对时间规则

  • 1分钟内:显示"刚刚"
  • 1-60分钟:显示"X分钟前"
  • 1-24小时:显示"X小时前"
  • 超过24小时:显示"X天前"

技术要点详解

1. 计算属性的高级应用

计算属性(Getter)可以根据对象状态动态返回值,避免数据冗余。

示例

dart 复制代码
class Vegetable {
  double price;
  double yesterdayPrice;
  
  // 计算属性:价格变化量
  double get priceChange => price - yesterdayPrice;
  
  // 计算属性:价格变化百分比
  double get priceChangePercent => (priceChange / yesterdayPrice) * 100;
  
  // 计算属性:是否涨价
  bool get isPriceUp => priceChange > 0;
  
  // 计算属性:是否降价
  bool get isPriceDown => priceChange < 0;
}

优势

  • 减少存储空间(不需要存储计算结果)
  • 保持数据一致性(总是基于最新数据计算)
  • 简化代码逻辑(使用时像访问属性一样)
  • 便于维护和扩展

使用场景

  • 价格涨跌判断
  • 颜色和图标映射
  • 格式化显示
  • 状态判断

NavigationBar是Material Design 3的底部导航组件,配合IndexedStack实现页面切换。

NavigationBar特点

  • Material Design 3风格
  • 自动处理选中状态
  • 支持图标和文字标签
  • 流畅的切换动画

IndexedStack特点

  • 保持所有子组件状态
  • 只显示指定索引的子组件
  • 切换时不重新构建
  • 提升用户体验

配合使用

dart 复制代码
int _selectedIndex = 0;

// 底部导航
NavigationBar(
  selectedIndex: _selectedIndex,
  onDestinationSelected: (index) {
    setState(() => _selectedIndex = index);
  },
  destinations: [...],
)

// 页面内容
IndexedStack(
  index: _selectedIndex,
  children: [
    Page1(),
    Page2(),
    Page3(),
  ],
)

3. ChoiceChip筛选组件

ChoiceChip是Material Design中用于单选或多选的芯片组件。

基本用法

dart 复制代码
ChoiceChip(
  label: Text('叶菜类'),
  selected: isSelected,
  onSelected: (selected) {
    setState(() {
      _selectedCategory = '叶菜类';
    });
  },
)

属性说明

  • label:显示的文本或组件
  • selected:是否选中
  • onSelected:选中状态改变回调
  • selectedColor:选中时的颜色
  • backgroundColor:未选中时的背景色
  • labelStyle:文本样式

使用场景

  • 分类筛选
  • 标签选择
  • 选项切换
  • 过滤条件

与FilterChip的区别

  • ChoiceChip:单选模式,适合互斥选项
  • FilterChip:多选模式,适合组合筛选

4. CustomPainter自定义绘制

CustomPainter是Flutter中用于自定义绘制的强大工具。

基本结构

dart 复制代码
class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 绘制逻辑
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;  // 是否需要重绘
  }
}

// 使用
CustomPaint(
  painter: MyPainter(),
  size: Size(width, height),
  child: Container(),
)

Canvas常用方法

方法 说明 示例
drawLine 绘制直线 canvas.drawLine(p1, p2, paint)
drawCircle 绘制圆形 canvas.drawCircle(center, radius, paint)
drawPath 绘制路径 canvas.drawPath(path, paint)
drawRect 绘制矩形 canvas.drawRect(rect, paint)
drawText 绘制文本 需要TextPainter

Paint属性

属性 说明
color 颜色 Colors.green
strokeWidth 线宽 2.0
style 样式 PaintingStyle.stroke / fill
strokeCap 线帽 StrokeCap.round / square / butt

使用场景

  • 图表绘制(折线图、柱状图、饼图)
  • 自定义形状
  • 动画效果
  • 数据可视化

5. Path路径绘制

Path用于创建复杂的绘制路径。

基本用法

dart 复制代码
final path = Path();
path.moveTo(x1, y1);  // 移动到起点
path.lineTo(x2, y2);  // 画线到终点
path.lineTo(x3, y3);  // 继续画线
path.close();         // 闭合路径

canvas.drawPath(path, paint);

常用方法

方法 说明 参数
moveTo 移动画笔 (x, y)
lineTo 画直线 (x, y)
quadraticBezierTo 二次贝塞尔曲线 (cx, cy, x, y)
cubicTo 三次贝塞尔曲线 (cx1, cy1, cx2, cy2, x, y)
arcTo 画圆弧 (rect, startAngle, sweepAngle, forceMoveTo)
addRect 添加矩形 (rect)
addOval 添加椭圆 (rect)
close 闭合路径

使用场景

  • 折线图
  • 曲线图
  • 自定义形状
  • 路径动画

6. 坐标系统转换

在绘制图表时,需要将数据坐标转换为画布坐标。

Y轴翻转

Canvas的Y轴向下为正,而数据的Y轴通常向上为正,需要翻转。

dart 复制代码
// 数据范围:minValue ~ maxValue
// 画布范围:0 ~ size.height

double dataToCanvasY(double value) {
  final range = maxValue - minValue;
  final ratio = (value - minValue) / range;
  return size.height - (ratio * size.height);
}

X轴均分

将数据点均匀分布在画布宽度上。

dart 复制代码
double dataToCanvasX(int index, int total) {
  return (size.width / (total - 1)) * index;
}

完整示例

dart 复制代码
for (int i = 0; i < dataPoints.length; i++) {
  final x = (size.width / (dataPoints.length - 1)) * i;
  final value = dataPoints[i];
  final y = size.height - ((value - minValue) / (maxValue - minValue)) * size.height;
  
  if (i == 0) {
    path.moveTo(x, y);
  } else {
    path.lineTo(x, y);
  }
}

7. List集合高级操作

Dart的List提供了丰富的函数式编程方法。

常用方法

方法 说明 示例
map 映射转换 list.map((e) => e * 2)
where 条件筛选 list.where((e) => e > 5)
reduce 归约计算 list.reduce((a, b) => a + b)
fold 带初始值归约 list.fold(0, (a, b) => a + b)
sort 排序 list.sort((a, b) => a.compareTo(b))
skip 跳过元素 list.skip(2)
take 取前N个 list.take(5)
any 是否存在 list.any((e) => e > 10)
every 是否全部满足 list.every((e) => e > 0)

链式操作

dart 复制代码
final result = vegetables
    .where((v) => v.category == '叶菜类')  // 筛选叶菜类
    .map((v) => v.price)                   // 提取价格
    .reduce((a, b) => a + b);              // 求和

统计示例

dart 复制代码
// 计算平均价格
final avgPrice = vegetables
    .map((v) => v.price)
    .reduce((a, b) => a + b) / vegetables.length;

// 查找最高价格
final maxPrice = vegetables
    .map((v) => v.price)
    .reduce((a, b) => a > b ? a : b);

// 查找最低价格
final minPrice = vegetables
    .map((v) => v.price)
    .reduce((a, b) => a < b ? a : b);

// 统计涨价数量
final upCount = vegetables
    .where((v) => v.isPriceUp)
    .length;

8. DateTime时间处理

Flutter中的DateTime类提供了丰富的时间处理功能。

创建时间

dart 复制代码
// 当前时间
final now = DateTime.now();

// 指定时间
final date = DateTime(2024, 1, 22, 14, 30);

// UTC时间
final utc = DateTime.utc(2024, 1, 22);

// 解析字符串
final parsed = DateTime.parse('2024-01-22 14:30:00');

时间计算

dart 复制代码
// 加时间
final tomorrow = now.add(Duration(days: 1));
final nextHour = now.add(Duration(hours: 1));

// 减时间
final yesterday = now.subtract(Duration(days: 1));

// 时间差
final diff = date1.difference(date2);
print(diff.inDays);     // 天数
print(diff.inHours);    // 小时数
print(diff.inMinutes);  // 分钟数

时间比较

dart 复制代码
if (date1.isAfter(date2)) {
  print('date1在date2之后');
}

if (date1.isBefore(date2)) {
  print('date1在date2之前');
}

if (date1.isAtSameMomentAs(date2)) {
  print('date1和date2相同');
}

时间格式化

dart 复制代码
// 补零格式化
final hour = date.hour.toString().padLeft(2, '0');
final minute = date.minute.toString().padLeft(2, '0');
final timeString = '$hour:$minute';

// 日期格式化
final dateString = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';

9. ModalBottomSheet对话框

ModalBottomSheet是从底部弹出的对话框,适合选择和筛选场景。

基本用法

dart 复制代码
showModalBottomSheet(
  context: context,
  builder: (context) => Container(
    padding: EdgeInsets.all(16),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text('选择城市'),
        // 内容
      ],
    ),
  ),
);

属性说明

  • context:上下文
  • builder:构建器函数
  • isScrollControlled:是否可滚动控制高度
  • backgroundColor:背景颜色
  • shape:形状(圆角等)
  • isDismissible:点击外部是否关闭
  • enableDrag:是否可拖动关闭

使用场景

  • 城市选择
  • 筛选条件
  • 选项列表
  • 操作菜单

关闭对话框

dart 复制代码
Navigator.pop(context);  // 关闭当前对话框
Navigator.pop(context, result);  // 关闭并返回结果

10. GridView网格布局

GridView用于创建网格布局,适合展示多列内容。

基本用法

dart 复制代码
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,        // 列数
    childAspectRatio: 2.5,    // 宽高比
    crossAxisSpacing: 8,      // 横向间距
    mainAxisSpacing: 8,       // 纵向间距
  ),
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Container(
      child: Text(items[index]),
    );
  },
)

两种Delegate

Delegate 说明 适用场景
FixedCrossAxisCount 固定列数 城市选择、标签展示
MaxCrossAxisExtent 最大宽度 图片网格、商品列表

FixedCrossAxisCount参数

  • crossAxisCount:列数
  • childAspectRatio:子项宽高比
  • crossAxisSpacing:横向间距
  • mainAxisSpacing:纵向间距

MaxCrossAxisExtent参数

  • maxCrossAxisExtent:最大宽度
  • childAspectRatio:子项宽高比
  • crossAxisSpacing:横向间距
  • mainAxisSpacing:纵向间距

功能扩展方向

1. 接入真实API

当前应用使用模拟数据,可以接入真实的蔬菜价格API。

实现步骤

  1. 选择数据源(农业部、批发市场API)
  2. 使用http包发起网络请求
  3. 解析JSON数据
  4. 更新数据模型
  5. 处理加载状态和错误

示例代码

dart 复制代码
import 'package:http/http.dart' as http;
import 'dart:convert';

Future<List<Vegetable>> fetchVegetables(String city) async {
  final response = await http.get(
    Uri.parse('https://api.example.com/vegetables?city=$city')
  );
  
  if (response.statusCode == 200) {
    final List<dynamic> data = json.decode(response.body);
    return data.map((json) => Vegetable.fromJson(json)).toList();
  } else {
    throw Exception('加载失败');
  }
}

2. 价格预警功能

当蔬菜价格超过设定阈值时,发送通知提醒用户。

功能设计

  • 设置价格上限和下限
  • 选择关注的蔬菜
  • 价格突破阈值时推送通知
  • 查看历史预警记录

实现要点

  • 使用SharedPreferences存储预警设置
  • 使用flutter_local_notifications发送通知
  • 定时检查价格变化
  • 记录预警历史

3. 收藏和对比功能

允许用户收藏常买的蔬菜,并对比不同市场的价格。

功能设计

  • 收藏蔬菜列表
  • 快速查看收藏蔬菜价格
  • 对比不同城市价格
  • 对比不同市场价格
  • 价格走势对比

实现要点

  • 使用SharedPreferences存储收藏列表
  • 创建收藏页面
  • 实现多城市数据查询
  • 使用图表对比展示

4. 购物清单功能

根据蔬菜价格生成购物清单,计算总价。

功能设计

  • 添加蔬菜到购物清单
  • 设置购买数量
  • 自动计算总价
  • 按价格排序
  • 导出购物清单

实现要点

  • 创建购物清单数据模型
  • 实现增删改查功能
  • 计算总价逻辑
  • 使用ListView展示清单

5. 价格走势分析

提供更详细的价格走势分析和预测。

功能设计

  • 30天价格走势图
  • 季节性价格分析
  • 价格波动统计
  • 价格预测(简单算法)
  • 最佳购买时机提示

实现要点

  • 存储历史价格数据
  • 使用CustomPainter绘制复杂图表
  • 实现简单的价格预测算法
  • 统计分析功能

6. 市场价格对比

对比不同批发市场的价格,找到最优惠的市场。

功能设计

  • 显示同一蔬菜在不同市场的价格
  • 标注最低价市场
  • 市场距离和导航
  • 市场营业时间
  • 市场评价和推荐

实现要点

  • 多市场数据查询
  • 价格排序和对比
  • 集成地图导航
  • 市场信息管理

7. 营养搭配建议

根据蔬菜营养成分,提供健康搭配建议。

功能设计

  • 蔬菜营养成分数据库
  • 营养搭配推荐
  • 每日营养目标
  • 食谱推荐
  • 健康饮食知识

实现要点

  • 建立营养数据库
  • 实现搭配算法
  • 创建食谱页面
  • 营养计算功能

8. 数据导出功能

将价格数据导出为Excel或PDF文件。

功能设计

  • 导出价格列表
  • 导出价格走势图
  • 导出购物清单
  • 选择导出格式(Excel/PDF/CSV)
  • 分享导出文件

实现要点

  • 使用excel包生成Excel文件
  • 使用pdf包生成PDF文件
  • 实现文件保存和分享
  • 格式化数据输出

常见问题解答

1. 如何获取真实的蔬菜价格数据?

问题:应用使用的是模拟数据,如何接入真实数据源?

解答

可以通过以下途径获取真实数据:

  1. 政府开放数据平台

    • 农业农村部数据平台
    • 各地农产品批发市场官网
    • 商务部市场监测系统
  2. 第三方API服务

    • 聚合数据API
    • 天行数据API
    • 自建爬虫采集数据
  3. 实现步骤

dart 复制代码
// 添加http依赖
dependencies:
  http: ^1.1.0

// 发起网络请求
Future<List<Vegetable>> fetchData() async {
  final response = await http.get(Uri.parse(apiUrl));
  if (response.statusCode == 200) {
    return parseData(response.body);
  }
  throw Exception('请求失败');
}

2. 价格数据多久更新一次?

问题:蔬菜价格应该多久更新一次比较合适?

解答

更新频率取决于数据源和应用场景:

  1. 批发市场价格

    • 每日更新1-2次
    • 早市和午市价格可能不同
    • 建议每6-12小时更新
  2. 零售价格

    • 每日更新1次即可
    • 价格相对稳定
    • 建议每天早上更新
  3. 实现方式

dart 复制代码
// 定时刷新
Timer.periodic(Duration(hours: 6), (timer) {
  _refreshPrices();
});

// 手动刷新
IconButton(
  onPressed: _refreshPrices,
  icon: Icon(Icons.refresh),
)

3. 如何实现离线查看功能?

问题:没有网络时如何查看蔬菜价格?

解答

可以使用本地缓存实现离线功能:

  1. 使用SharedPreferences
dart 复制代码
// 保存数据
final prefs = await SharedPreferences.getInstance();
await prefs.setString('vegetables', jsonEncode(vegetables));

// 读取数据
final data = prefs.getString('vegetables');
if (data != null) {
  vegetables = jsonDecode(data);
}
  1. 使用sqflite数据库
dart 复制代码
// 创建数据库
final database = await openDatabase('vegetables.db');

// 插入数据
await database.insert('vegetables', vegetable.toMap());

// 查询数据
final List<Map> maps = await database.query('vegetables');
  1. 缓存策略
    • 优先使用缓存数据
    • 后台更新最新数据
    • 显示数据更新时间
    • 提示用户数据可能过期

4. 如何实现多城市价格对比?

问题:想同时查看多个城市的蔬菜价格进行对比。

解答

可以创建价格对比页面:

  1. 数据结构设计
dart 复制代码
class CityPriceComparison {
  String vegetableName;
  Map<String, double> cityPrices;  // 城市 -> 价格
  
  String get cheapestCity {
    return cityPrices.entries
        .reduce((a, b) => a.value < b.value ? a : b)
        .key;
  }
}
  1. UI实现
dart 复制代码
Widget _buildComparisonTable() {
  return DataTable(
    columns: [
      DataColumn(label: Text('蔬菜')),
      DataColumn(label: Text('北京')),
      DataColumn(label: Text('上海')),
      DataColumn(label: Text('广州')),
    ],
    rows: vegetables.map((veg) {
      return DataRow(cells: [
        DataCell(Text(veg.name)),
        DataCell(Text('¥${veg.beijingPrice}')),
        DataCell(Text('¥${veg.shanghaiPrice}')),
        DataCell(Text('¥${veg.guangzhouPrice}')),
      ]);
    }).toList(),
  );
}
  1. 功能特点
    • 横向对比多个城市
    • 标注最低价城市
    • 计算价格差异
    • 排序和筛选

5. 如何导出价格数据?

问题:想把价格数据导出为Excel或PDF文件。

解答

可以使用相关包实现数据导出:

  1. 导出Excel
dart 复制代码
// 添加依赖
dependencies:
  excel: ^4.0.0

// 导出代码
import 'package:excel/excel.dart';

Future<void> exportToExcel() async {
  var excel = Excel.createExcel();
  Sheet sheet = excel['价格表'];
  
  // 添加表头
  sheet.appendRow(['蔬菜名称', '价格', '单位', '市场']);
  
  // 添加数据
  for (var veg in vegetables) {
    sheet.appendRow([veg.name, veg.price, veg.unit, veg.market]);
  }
  
  // 保存文件
  var bytes = excel.encode();
  File('vegetables.xlsx').writeAsBytesSync(bytes!);
}
  1. 导出PDF
dart 复制代码
// 添加依赖
dependencies:
  pdf: ^3.10.0

// 导出代码
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

Future<void> exportToPdf() async {
  final pdf = pw.Document();
  
  pdf.addPage(
    pw.Page(
      build: (context) => pw.Table(
        children: [
          pw.TableRow(
            children: [
              pw.Text('蔬菜名称'),
              pw.Text('价格'),
              pw.Text('单位'),
            ],
          ),
          ...vegetables.map((veg) => pw.TableRow(
            children: [
              pw.Text(veg.name),
              pw.Text('¥${veg.price}'),
              pw.Text(veg.unit),
            ],
          )),
        ],
      ),
    ),
  );
  
  final file = File('vegetables.pdf');
  await file.writeAsBytes(await pdf.save());
}
  1. 分享功能
dart 复制代码
// 添加依赖
dependencies:
  share_plus: ^7.0.0

// 分享文件
import 'package:share_plus/share_plus.dart';

await Share.shareXFiles([XFile('vegetables.xlsx')]);

项目总结

核心功能流程图

启动应用
生成蔬菜数据
显示价格列表
用户操作
搜索蔬菜
分类筛选
切换城市
刷新价格
查看详情
显示详情页
价格走势图
营养信息
市场信息

数据流程图

原始数据
数据生成
全部蔬菜列表
应用筛选条件
筛选后列表
UI展示
用户交互
操作类型
搜索
筛选
刷新
切换城市

技术架构图

Flutter应用
UI层
数据层
工具层
主页面
详情页面
自定义组件
价格列表
分类统计
价格趋势
蔬菜卡片
城市选择器
价格图表
数据模型
数据生成
数据筛选
时间格式化
价格计算
统计分析

项目特色总结

  1. 完整的功能体系

    • 多城市查询
    • 智能搜索筛选
    • 价格趋势分析
    • 详细信息展示
  2. 优秀的用户体验

    • Material Design 3设计
    • 流畅的页面切换
    • 直观的数据可视化
    • 友好的交互反馈
  3. 清晰的代码结构

    • 数据模型分离
    • 计算属性封装
    • 组件化开发
    • 易于维护扩展
  4. 实用的技术方案

    • 无需额外依赖
    • 模拟数据生成
    • 自定义图表绘制
    • 响应式布局

学习收获

通过本项目,你将掌握:

  1. Flutter核心组件

    • NavigationBar底部导航
    • IndexedStack页面管理
    • ChoiceChip筛选组件
    • GridView网格布局
    • ModalBottomSheet对话框
  2. 自定义绘制技术

    • CustomPainter基础
    • Canvas绘制方法
    • Path路径操作
    • 坐标系统转换
  3. 数据处理技巧

    • 计算属性应用
    • List集合操作
    • 数据筛选排序
    • 统计分析方法
  4. 状态管理实践

    • setState状态更新
    • 数据流管理
    • 页面状态保持
    • 交互响应处理
  5. 时间处理技能

    • DateTime操作
    • 时间格式化
    • 相对时间显示
    • 时间差计算

性能优化建议

  1. 列表优化

    • 使用ListView.builder懒加载
    • 避免在build中创建大量对象
    • 合理使用const构造函数
  2. 图表优化

    • 限制数据点数量
    • 使用RepaintBoundary隔离重绘
    • shouldRepaint返回精确判断
  3. 数据优化

    • 缓存计算结果
    • 避免重复计算
    • 使用计算属性
  4. 内存优化

    • 及时释放资源
    • 避免内存泄漏
    • 合理使用缓存

未来优化方向

  1. 功能增强

    • 接入真实API
    • 添加价格预警
    • 实现收藏功能
    • 支持数据导出
  2. 体验提升

    • 添加加载动画
    • 优化错误处理
    • 支持主题切换
    • 添加引导页面
  3. 性能优化

    • 实现数据缓存
    • 优化图表渲染
    • 减少重建次数
    • 提升响应速度
  4. 平台适配

    • 适配鸿蒙系统
    • 优化平板布局
    • 支持横屏模式
    • 适配不同分辨率

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
Easonmax2 小时前
小白基础入门 React Native 鸿蒙跨平台开发:用基础知识模拟一个——系统设置页面
react native·react.js·harmonyos
前端不太难2 小时前
HarmonyOS 后台机制,对实时游戏意味着什么?
游戏·状态模式·harmonyos
Swift社区2 小时前
HarmonyOS 中 MindSpore Lite 源码编译转换工具环境与步骤
华为·harmonyos·arkui
小雨青年2 小时前
鸿蒙 HarmonyOS 6 | 系统能力 (05):后台任务开发 长时任务、WorkScheduler 与代理提醒详解
华为·harmonyos
深海的鲸同学 luvi2 小时前
你如何证实鸿蒙是安卓改
安卓·harmonyos
zilikew2 小时前
Flutter框架跨平台鸿蒙开发——小票管家APP开发流程
flutter·华为·harmonyos·鸿蒙
Easonmax2 小时前
【鸿蒙pc命令行适配】OpenSSL主流版本介绍以及为何推荐移植OpenSSL 3.5版本
服务器·华为·harmonyos
Easonmax3 小时前
小白学习React Native 鸿蒙跨平台开发:实现一个简单的商品评价页面
学习·react native·harmonyos