Flutter香薰养护记录器应用开发教程

项目概述

本教程将带你开发一个功能完整的Flutter香薰养护记录器应用。这款应用专为香薰爱好者设计,提供香薰收藏管理、使用记录追踪、养护提醒和统计分析等功能,帮助用户更好地管理和享受香薰生活。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

应用特色

  • 香薰收藏管理:记录香薰的详细信息,包括品牌、类型、香调、容量等
  • 库存状态监控:实时显示剩余量,智能提醒库存不足和过期情况
  • 使用记录追踪:记录每次使用的时长、用量、心情和评分
  • 养护记录管理:记录清洁、补充、更换等养护操作
  • 智能筛选搜索:支持按类型、香调、收藏状态等多维度筛选
  • 统计分析功能:提供使用统计、库存分析、类型分布等数据洞察
  • 个性化标签:自定义标签系统,便于分类管理

技术栈

  • 框架:Flutter 3.x
  • 语言:Dart
  • UI组件:Material Design 3
  • 状态管理:StatefulWidget
  • 动画:AnimationController + FadeTransition
  • 数据存储:内存存储(可扩展为本地数据库)
  • 导航:TabBar + NavigationBar

项目结构设计

核心数据模型

1. 香薰信息模型(AromaItem)
class AromaItem {
  final String id;              // 唯一标识
  final String name;            // 香薰名称
  final String brand;           // 品牌
  final String type;            // 类型(精油、蜡烛等)
  final String scent;           // 香调
  final double capacity;        // 容量(ml)
  final double currentAmount;   // 当前剩余量
  final DateTime purchaseDate;  // 购买日期
  final DateTime? expiryDate;   // 过期日期
  final double price;           // 价格
  final String notes;           // 备注
  final List<String> tags;      // 标签
  final String imageUrl;        // 图片URL
  bool isFavorite;             // 是否收藏
  int usageCount;              // 使用次数
  DateTime? lastUsedDate;      // 最后使用日期
}
2. 使用记录模型(UsageRecord)
class UsageRecord {
  final String id;              // 唯一标识
  final String aromaId;         // 关联的香薰ID
  final DateTime usageDate;     // 使用日期
  final Duration duration;      // 使用时长
  final double amountUsed;      // 使用量
  final String mood;            // 心情
  final String location;        // 使用地点
  final String notes;           // 备注
  final int rating;             // 评分 1-5
}
3. 养护记录模型(CareRecord)
class CareRecord {
  final String id;              // 唯一标识
  final String aromaId;         // 关联的香薰ID
  final DateTime careDate;      // 养护日期
  final String careType;        // 养护类型(清洁、补充、更换等)
  final String description;     // 描述
  final String notes;           // 备注
}

枚举定义

香薰类型枚举
enum AromaType {
  essentialOil,  // 精油
  candle,        // 蜡烛
  diffuserStone, // 扩香石
  reedDiffuser,  // 藤条扩香
  incense,       // 香薰
  potpourri,     // 干花香薰
}
香调分类枚举
enum ScentCategory {
  floral,    // 花香调
  woody,     // 木质调
  citrus,    // 柑橘调
  herbal,    // 草本调
  oriental,  // 东方调
  fresh,     // 清新调
  spicy,     // 辛香调
  sweet,     // 甜香调
}

页面架构

应用采用底部导航栏设计,包含四个主要页面:

  1. 香薰页面:展示所有香薰收藏,支持搜索和筛选
  2. 使用记录页面:记录和查看香薰使用历史
  3. 养护记录页面:记录香薰的清洁和维护操作
  4. 统计页面:展示使用统计和数据分析

详细实现步骤

第一步:项目初始化

创建新的Flutter项目:

flutter create aroma_care_app
cd aroma_care_app

第二步:主应用结构

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '香薰养护记录器',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
        useMaterial3: true,
      ),
      home: const AromaCareHomePage(),
    );
  }
}

第三步:数据初始化

创建示例香薰数据:

void _initializeData() {
  _aromaItems = [
    AromaItem(
      id: '1',
      name: '薰衣草精油',
      brand: 'Young Living',
      type: _getAromaTypeName(AromaType.essentialOil),
      scent: _getScentCategoryName(ScentCategory.floral),
      capacity: 15.0,
      currentAmount: 12.0,
      purchaseDate: DateTime.now().subtract(const Duration(days: 60)),
      expiryDate: DateTime.now().add(const Duration(days: 365)),
      price: 89.0,
      notes: '来自法国普罗旺斯的有机薰衣草,具有舒缓放松的效果',
      tags: ['舒缓', '助眠', '有机'],
      imageUrl: 'lavender_oil.jpg',
      isFavorite: true,
      usageCount: 15,
      lastUsedDate: DateTime.now().subtract(const Duration(days: 2)),
    ),
    // 更多香薰数据...
  ];
}

第四步:香薰列表页面

香薰卡片组件
Widget _buildAromaCard(AromaItem item) {
  return Card(
    elevation: 4,
    margin: const EdgeInsets.only(bottom: 16),
    child: InkWell(
      onTap: () => _showAromaDetail(item),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 标题行
            Row(
              children: [
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        item.name,
                        style: const TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      Text(
                        '${item.brand}${item.type}',
                        style: TextStyle(
                          color: Colors.grey.shade600,
                          fontSize: 14,
                        ),
                      ),
                    ],
                  ),
                ),
                IconButton(
                  icon: Icon(
                    item.isFavorite ? Icons.favorite : Icons.favorite_border,
                    color: item.isFavorite ? Colors.red : Colors.grey,
                  ),
                  onPressed: () => _toggleFavorite(item),
                ),
              ],
            ),

            // 香调和标签
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: [
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  decoration: BoxDecoration(
                    color: _getScentColor(item.scent),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(
                    item.scent,
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 12,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                // 标签显示...
              ],
            ),

            // 剩余量进度条
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text('剩余量'),
                    Text('${item.currentAmount}ml / ${item.capacity}ml'),
                  ],
                ),
                LinearProgressIndicator(
                  value: item.remainingPercentage,
                  backgroundColor: Colors.grey.shade300,
                  valueColor: AlwaysStoppedAnimation<Color>(
                    item.isLowStock ? Colors.red : Colors.green,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    ),
  );
}
智能状态检测
// 获取剩余百分比
double get remainingPercentage => currentAmount / capacity;

// 是否即将过期(30天内)
bool get isExpiringSoon {
  if (expiryDate == null) return false;
  final daysUntilExpiry = expiryDate!.difference(DateTime.now()).inDays;
  return daysUntilExpiry <= 30 && daysUntilExpiry > 0;
}

// 是否已过期
bool get isExpired {
  if (expiryDate == null) return false;
  return expiryDate!.isBefore(DateTime.now());
}

// 是否库存不足(少于20%)
bool get isLowStock => remainingPercentage < 0.2;

第五步:搜索和筛选功能

多维度筛选
List<AromaItem> _getFilteredAromaItems() {
  return _aromaItems.where((item) {
    // 搜索过滤
    if (_searchQuery.isNotEmpty) {
      final query = _searchQuery.toLowerCase();
      if (!item.name.toLowerCase().contains(query) &&
          !item.brand.toLowerCase().contains(query) &&
          !item.scent.toLowerCase().contains(query) &&
          !item.tags.any((tag) => tag.toLowerCase().contains(query))) {
        return false;
      }
    }

    // 类型过滤
    if (_selectedType != null &&
        item.type != _getAromaTypeName(_selectedType!)) {
      return false;
    }

    // 香调过滤
    if (_selectedScent != null &&
        item.scent != _getScentCategoryName(_selectedScent!)) {
      return false;
    }

    // 收藏过滤
    if (_showFavoritesOnly && !item.isFavorite) {
      return false;
    }

    // 库存不足过滤
    if (_showLowStockOnly && !item.isLowStock) {
      return false;
    }

    return true;
  }).toList();
}
筛选对话框
void _showFilterDialog() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('筛选香薰'),
      content: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('类型:'),
            Wrap(
              spacing: 8,
              children: [
                FilterChip(
                  label: const Text('全部'),
                  selected: _selectedType == null,
                  onSelected: (selected) {
                    setState(() {
                      _selectedType = selected ? null : _selectedType;
                    });
                  },
                ),
                ...AromaType.values.map((type) => FilterChip(
                      label: Text(_getAromaTypeName(type)),
                      selected: _selectedType == type,
                      onSelected: (selected) {
                        setState(() {
                          _selectedType = selected ? type : null;
                        });
                      },
                    )),
              ],
            ),
            // 更多筛选选项...
          ],
        ),
      ),
    ),
  );
}

第六步:使用记录功能

使用记录卡片
Widget _buildUsageRecordCard(UsageRecord record, AromaItem aroma) {
  return Card(
    elevation: 2,
    margin: const EdgeInsets.only(bottom: 12),
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 标题行
          Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      aroma.name,
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Text(
                      _formatDateTime(record.usageDate),
                      style: TextStyle(
                        color: Colors.grey.shade600,
                        fontSize: 12,
                      ),
                    ),
                  ],
                ),
              ),
              // 评分星星
              Row(
                children: List.generate(5, (index) {
                  return Icon(
                    index < record.rating ? Icons.star : Icons.star_border,
                    color: Colors.amber,
                    size: 16,
                  );
                }),
              ),
            ],
          ),

          // 使用信息
          Row(
            children: [
              _buildInfoItem(
                Icons.access_time,
                '时长',
                '${record.duration.inHours}h${record.duration.inMinutes % 60}m',
                Colors.blue,
              ),
              const SizedBox(width: 16),
              _buildInfoItem(
                Icons.opacity,
                '用量',
                '${record.amountUsed}ml',
                Colors.green,
              ),
              const SizedBox(width: 16),
              _buildInfoItem(
                Icons.mood,
                '心情',
                record.mood,
                Colors.orange,
              ),
            ],
          ),

          // 地点和备注
          Row(
            children: [
              Icon(Icons.location_on, size: 16, color: Colors.grey.shade600),
              const SizedBox(width: 4),
              Text(record.location),
            ],
          ),
          if (record.notes.isNotEmpty)
            Text(record.notes),
        ],
      ),
    ),
  );
}
信息展示组件
Widget _buildInfoItem(IconData icon, String label, String value, Color color) {
  return Column(
    children: [
      Icon(icon, size: 20, color: color),
      const SizedBox(height: 4),
      Text(
        label,
        style: TextStyle(
          fontSize: 10,
          color: Colors.grey.shade600,
        ),
      ),
      Text(
        value,
        style: TextStyle(
          fontSize: 12,
          fontWeight: FontWeight.bold,
          color: color,
        ),
      ),
    ],
  );
}

第七步:养护记录功能

养护记录卡片
Widget _buildCareRecordCard(CareRecord record, AromaItem aroma) {
  return Card(
    elevation: 2,
    margin: const EdgeInsets.only(bottom: 12),
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 标题行
          Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      aroma.name,
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Text(
                      _formatDateTime(record.careDate),
                      style: TextStyle(
                        color: Colors.grey.shade600,
                        fontSize: 12,
                      ),
                    ),
                  ],
                ),
              ),
              Container(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                decoration: BoxDecoration(
                  color: _getCareTypeColor(record.careType),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Text(
                  record.careType,
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 12,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ],
          ),

          // 描述和备注
          Text(
            record.description,
            style: const TextStyle(
              fontSize: 14,
              fontWeight: FontWeight.w500,
            ),
          ),
          if (record.notes.isNotEmpty)
            Text(
              record.notes,
              style: TextStyle(
                color: Colors.grey.shade700,
                fontSize: 12,
              ),
            ),
        ],
      ),
    ),
  );
}
养护类型颜色映射
Color _getCareTypeColor(String careType) {
  switch (careType) {
    case '清洁':
      return Colors.blue;
    case '补充':
      return Colors.green;
    case '更换':
      return Colors.orange;
    case '维修':
      return Colors.red;
    default:
      return Colors.grey;
  }
}

第八步:统计分析功能

统计卡片组件
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
  return Container(
    padding: const EdgeInsets.all(12),
    margin: const EdgeInsets.all(4),
    decoration: BoxDecoration(
      color: color.withValues(alpha: 0.1),
      borderRadius: BorderRadius.circular(8),
      border: Border.all(color: color.withValues(alpha: 0.3)),
    ),
    child: Column(
      children: [
        Icon(icon, color: color, size: 24),
        const SizedBox(height: 8),
        Text(
          value,
          style: TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
            color: color,
          ),
        ),
        Text(
          title,
          style: TextStyle(
            fontSize: 12,
            color: color,
          ),
          textAlign: TextAlign.center,
        ),
      ],
    ),
  );
}
数据统计计算
// 获取最常用的香薰
String _getMostUsedAroma() {
  if (_aromaItems.isEmpty) return '无';
  
  final mostUsed = _aromaItems.reduce((a, b) => 
      a.usageCount > b.usageCount ? a : b);
  
  return mostUsed.usageCount > 0 ? mostUsed.name : '无';
}

// 获取类型分布
Map<String, int> _getTypeDistribution() {
  final distribution = <String, int>{};
  for (final item in _aromaItems) {
    distribution[item.type] = (distribution[item.type] ?? 0) + 1;
  }
  return distribution;
}

// 获取香调分布
Map<String, int> _getScentDistribution() {
  final distribution = <String, int>{};
  for (final item in _aromaItems) {
    distribution[item.scent] = (distribution[item.scent] ?? 0) + 1;
  }
  return distribution;
}
分布图表展示
// 类型分布图表
..._getTypeDistribution().entries.map((entry) {
  final percentage = entry.value / _aromaItems.length;
  return Padding(
    padding: const EdgeInsets.only(bottom: 12),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(entry.key),
            Text('${entry.value}个 (${(percentage * 100).toStringAsFixed(1)}%)'),
          ],
        ),
        const SizedBox(height: 4),
        LinearProgressIndicator(
          value: percentage,
          backgroundColor: Colors.grey.shade300,
          valueColor: AlwaysStoppedAnimation<Color>(
            _getTypeColor(entry.key),
          ),
        ),
      ],
    ),
  );
}),

第九步:香薰详情页面

TabBar导航设计
class AromaDetailPage extends StatefulWidget {
  final AromaItem item;
  final List<UsageRecord> usageRecords;
  final List<CareRecord> careRecords;
  final Function(AromaItem) onUpdate;
  final Function(UsageRecord) onAddUsage;
  final Function(CareRecord) onAddCare;

  
  State<AromaDetailPage> createState() => _AromaDetailPageState();
}

class _AromaDetailPageState extends State<AromaDetailPage>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.item.name),
        actions: [
          IconButton(
            icon: Icon(
              widget.item.isFavorite ? Icons.favorite : Icons.favorite_border,
              color: widget.item.isFavorite ? Colors.red : null,
            ),
            onPressed: () {
              setState(() {
                widget.item.isFavorite = !widget.item.isFavorite;
              });
              widget.onUpdate(widget.item);
            },
          ),
        ],
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: '详情'),
            Tab(text: '使用记录'),
            Tab(text: '养护记录'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          _buildDetailTab(),
          _buildUsageTab(),
          _buildCareTab(),
        ],
      ),
    );
  }
}
详情页面内容
Widget _buildDetailTab() {
  return SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 基本信息卡片
        Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '基本信息',
                  style: Theme.of(context).textTheme.titleLarge?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                ),
                const SizedBox(height: 16),
                _buildDetailRow('名称', widget.item.name),
                _buildDetailRow('品牌', widget.item.brand),
                _buildDetailRow('类型', widget.item.type),
                _buildDetailRow('香调', widget.item.scent),
                _buildDetailRow('容量', '${widget.item.capacity}ml'),
                _buildDetailRow('价格', ${widget.item.price.toStringAsFixed(0)}'),
                _buildDetailRow('购买日期', _formatDate(widget.item.purchaseDate)),
                if (widget.item.expiryDate != null)
                  _buildDetailRow('过期日期', _formatDate(widget.item.expiryDate!)),
              ],
            ),
          ),
        ),

        // 库存状态卡片
        Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('库存状态'),
                const SizedBox(height: 16),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text('剩余量'),
                    Text(
                      '${widget.item.currentAmount.toStringAsFixed(1)}ml / ${widget.item.capacity.toStringAsFixed(1)}ml',
                      style: const TextStyle(fontWeight: FontWeight.bold),
                    ),
                  ],
                ),
                const SizedBox(height: 8),
                LinearProgressIndicator(
                  value: widget.item.remainingPercentage,
                  backgroundColor: Colors.grey.shade300,
                  valueColor: AlwaysStoppedAnimation<Color>(
                    widget.item.isLowStock ? Colors.red : Colors.green,
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  '剩余 ${(widget.item.remainingPercentage * 100).toStringAsFixed(1)}%',
                  style: TextStyle(
                    color: widget.item.isLowStock ? Colors.red : Colors.green,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
          ),
        ),

        // 标签和备注卡片
        Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('标签'),
                const SizedBox(height: 12),
                Wrap(
                  spacing: 8,
                  runSpacing: 8,
                  children: widget.item.tags.map((tag) => Chip(
                        label: Text(tag),
                        backgroundColor: Colors.purple.withValues(alpha: 0.1),
                        labelStyle: TextStyle(
                          color: Colors.purple.shade700,
                        ),
                      )).toList(),
                ),
                const SizedBox(height: 16),
                Text('备注'),
                const SizedBox(height: 8),
                Text(
                  widget.item.notes.isEmpty ? '暂无备注' : widget.item.notes,
                  style: TextStyle(
                    color: widget.item.notes.isEmpty 
                        ? Colors.grey.shade600 
                        : Colors.black87,
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  );
}

第十步:动画效果实现

淡入动画
void _setupAnimations() {
  _fadeAnimationController = AnimationController(
    duration: const Duration(milliseconds: 800),
    vsync: this,
  );

  _fadeAnimation = Tween<double>(
    begin: 0.0,
    end: 1.0,
  ).animate(CurvedAnimation(
    parent: _fadeAnimationController,
    curve: Curves.easeInOut,
  ));

  _fadeAnimationController.forward();
}

// 在build方法中使用
body: FadeTransition(
  opacity: _fadeAnimation,
  child: IndexedStack(
    index: _selectedIndex,
    children: [
      _buildAromaListPage(),
      _buildUsageRecordsPage(),
      _buildCareRecordsPage(),
      _buildStatisticsPage(),
    ],
  ),
),

核心功能详解

1. 智能状态监控

应用提供了多种智能状态检测功能:

// 剩余量百分比计算
double get remainingPercentage => currentAmount / capacity;

// 库存不足检测(低于20%)
bool get isLowStock => remainingPercentage < 0.2;

// 即将过期检测(30天内)
bool get isExpiringSoon {
  if (expiryDate == null) return false;
  final daysUntilExpiry = expiryDate!.difference(DateTime.now()).inDays;
  return daysUntilExpiry <= 30 && daysUntilExpiry > 0;
}

// 已过期检测
bool get isExpired {
  if (expiryDate == null) return false;
  return expiryDate!.isBefore(DateTime.now());
}

2. 颜色主题系统

根据香调和类型动态设置颜色:

Color _getScentColor(String scent) {
  switch (scent) {
    case '花香调': return Colors.pink;
    case '木质调': return Colors.brown;
    case '柑橘调': return Colors.orange;
    case '草本调': return Colors.green;
    case '东方调': return Colors.purple;
    case '清新调': return Colors.blue;
    case '辛香调': return Colors.red;
    case '甜香调': return Colors.amber;
    default: return Colors.grey;
  }
}

Color _getTypeColor(String type) {
  switch (type) {
    case '精油': return Colors.green;
    case '蜡烛': return Colors.orange;
    case '扩香石': return Colors.blue;
    case '藤条扩香': return Colors.purple;
    case '香薰': return Colors.brown;
    case '干花香薰': return Colors.pink;
    default: return Colors.grey;
  }
}

3. 数据统计算法

// 计算平均评分
double getAverageRating() {
  if (_usageRecords.isEmpty) return 0.0;
  return _usageRecords.fold(0, (sum, record) => sum + record.rating) / 
         _usageRecords.length;
}

// 计算总投入
double getTotalInvestment() {
  return _aromaItems.fold(0.0, (sum, item) => sum + item.price);
}

// 获取使用频率最高的香薰
AromaItem? getMostUsedAroma() {
  if (_aromaItems.isEmpty) return null;
  return _aromaItems.reduce((a, b) => 
      a.usageCount > b.usageCount ? a : b);
}

4. 搜索算法优化

bool _matchesSearchQuery(AromaItem item, String query) {
  final lowerQuery = query.toLowerCase();
  
  // 名称匹配
  if (item.name.toLowerCase().contains(lowerQuery)) return true;
  
  // 品牌匹配
  if (item.brand.toLowerCase().contains(lowerQuery)) return true;
  
  // 香调匹配
  if (item.scent.toLowerCase().contains(lowerQuery)) return true;
  
  // 标签匹配
  if (item.tags.any((tag) => tag.toLowerCase().contains(lowerQuery))) return true;
  
  // 备注匹配
  if (item.notes.toLowerCase().contains(lowerQuery)) return true;
  
  return false;
}

性能优化

1. 列表优化

使用ListView.builder实现虚拟滚动:

ListView.builder(
  padding: const EdgeInsets.all(16),
  itemCount: filteredItems.length,
  itemBuilder: (context, index) {
    final item = filteredItems[index];
    return _buildAromaCard(item);
  },
)

2. 状态管理优化

合理使用setState,避免不必要的重建:

void _toggleFavorite(AromaItem item) {
  setState(() {
    item.isFavorite = !item.isFavorite;
  });
  
  // 只更新必要的UI部分
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(item.isFavorite ? '已添加到收藏' : '已从收藏中移除'),
      duration: const Duration(seconds: 1),
    ),
  );
}

3. 内存管理

及时释放动画控制器:


void dispose() {
  _fadeAnimationController.dispose();
  _tabController.dispose();
  super.dispose();
}

4. 数据缓存策略

// 缓存筛选结果
List<AromaItem>? _cachedFilteredItems;
String? _lastSearchQuery;

List<AromaItem> _getFilteredAromaItems() {
  // 如果搜索条件没有变化,返回缓存结果
  if (_cachedFilteredItems != null && _lastSearchQuery == _searchQuery) {
    return _cachedFilteredItems!;
  }
  
  // 重新计算筛选结果
  _cachedFilteredItems = _aromaItems.where((item) {
    // 筛选逻辑...
  }).toList();
  
  _lastSearchQuery = _searchQuery;
  return _cachedFilteredItems!;
}

扩展功能

1. 数据持久化

使用shared_preferences保存数据:

dependencies:
  shared_preferences: ^2.2.0

// 保存香薰数据
Future<void> _saveAromaData() async {
  final prefs = await SharedPreferences.getInstance();
  final aromaJson = _aromaItems.map((item) => item.toJson()).toList();
  await prefs.setString('aroma_items', jsonEncode(aromaJson));
}

// 加载香薰数据
Future<void> _loadAromaData() async {
  final prefs = await SharedPreferences.getInstance();
  final aromaJsonString = prefs.getString('aroma_items');
  if (aromaJsonString != null) {
    final aromaJsonList = jsonDecode(aromaJsonString) as List;
    _aromaItems = aromaJsonList
        .map((json) => AromaItem.fromJson(json))
        .toList();
  }
}

2. 图片上传功能

集成image_picker插件:

dependencies:
  image_picker: ^1.0.4

Future<void> _pickImage() async {
  final picker = ImagePicker();
  final pickedFile = await picker.pickImage(source: ImageSource.gallery);
  
  if (pickedFile != null) {
    setState(() {
      // 更新香薰图片
    });
  }
}

3. 提醒通知功能

使用flutter_local_notifications:

dependencies:
  flutter_local_notifications: ^16.1.0

// 设置过期提醒
Future<void> _scheduleExpiryReminder(AromaItem item) async {
  if (item.expiryDate == null) return;
  
  final reminderDate = item.expiryDate!.subtract(const Duration(days: 7));
  
  await flutterLocalNotificationsPlugin.schedule(
    item.id.hashCode,
    '香薰即将过期',
    '${item.name} 将在一周后过期,请及时使用',
    reminderDate,
    const NotificationDetails(
      android: AndroidNotificationDetails(
        'aroma_expiry',
        '过期提醒',
        channelDescription: '香薰过期提醒',
        importance: Importance.high,
        priority: Priority.high,
      ),
    ),
  );
}

// 设置库存不足提醒
Future<void> _scheduleLowStockReminder(AromaItem item) async {
  if (!item.isLowStock) return;
  
  await flutterLocalNotificationsPlugin.show(
    item.id.hashCode + 1000,
    '库存不足提醒',
    '${item.name} 库存不足,剩余${item.currentAmount}ml',
    const NotificationDetails(
      android: AndroidNotificationDetails(
        'aroma_low_stock',
        '库存提醒',
        channelDescription: '香薰库存不足提醒',
        importance: Importance.high,
        priority: Priority.high,
      ),
    ),
  );
}

4. 数据导出功能

dependencies:
  path_provider: ^2.1.1
  csv: ^5.0.2

Future<void> _exportToCSV() async {
  List<List<dynamic>> rows = [];
  
  // 添加表头
  rows.add([
    '名称', '品牌', '类型', '香调', '容量', '剩余量', 
    '购买日期', '过期日期', '价格', '使用次数', '最后使用'
  ]);
  
  // 添加数据行
  for (final item in _aromaItems) {
    rows.add([
      item.name,
      item.brand,
      item.type,
      item.scent,
      item.capacity,
      item.currentAmount,
      item.purchaseDate.toIso8601String(),
      item.expiryDate?.toIso8601String() ?? '',
      item.price,
      item.usageCount,
      item.lastUsedDate?.toIso8601String() ?? '',
    ]);
  }
  
  String csv = const ListToCsvConverter().convert(rows);
  
  // 保存到文件
  final directory = await getApplicationDocumentsDirectory();
  final file = File('${directory.path}/aroma_data.csv');
  await file.writeAsString(csv);
  
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text('数据已导出到 ${file.path}')),
  );
}

测试策略

1. 单元测试

测试核心业务逻辑:

// test/models/aroma_item_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:aroma_care_app/models/aroma_item.dart';

void main() {
  group('AromaItem', () {
    test('should calculate remaining percentage correctly', () {
      final item = AromaItem(
        id: '1',
        name: 'Test Aroma',
        brand: 'Test Brand',
        type: '精油',
        scent: '花香调',
        capacity: 100.0,
        currentAmount: 25.0,
        purchaseDate: DateTime.now(),
        price: 50.0,
        notes: '',
        tags: [],
        imageUrl: '',
      );

      expect(item.remainingPercentage, 0.25);
      expect(item.isLowStock, true);
    });

    test('should detect expiry correctly', () {
      final item = AromaItem(
        id: '1',
        name: 'Test Aroma',
        brand: 'Test Brand',
        type: '精油',
        scent: '花香调',
        capacity: 100.0,
        currentAmount: 50.0,
        purchaseDate: DateTime.now(),
        expiryDate: DateTime.now().add(const Duration(days: 15)),
        price: 50.0,
        notes: '',
        tags: [],
        imageUrl: '',
      );

      expect(item.isExpiringSoon, true);
      expect(item.isExpired, false);
    });
  });
}

2. Widget测试

测试UI组件:

// test/widgets/aroma_card_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:aroma_care_app/main.dart';

void main() {
  group('AromaCard Widget Tests', () {
    testWidgets('should display aroma information', (WidgetTester tester) async {
      await tester.pumpWidget(const MyApp());
      
      // 验证香薰名称显示
      expect(find.text('薰衣草精油'), findsOneWidget);
      
      // 验证品牌信息显示
      expect(find.text('Young Living'), findsOneWidget);
      
      // 验证收藏按钮
      expect(find.byIcon(Icons.favorite), findsWidgets);
    });

    testWidgets('should toggle favorite when heart icon is tapped', 
        (WidgetTester tester) async {
      await tester.pumpWidget(const MyApp());
      
      // 点击收藏按钮
      await tester.tap(find.byIcon(Icons.favorite_border).first);
      await tester.pump();
      
      // 验证状态变化
      expect(find.byIcon(Icons.favorite), findsWidgets);
    });
  });
}

3. 集成测试

测试完整用户流程:

// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:aroma_care_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Aroma Care App Integration Tests', () {
    testWidgets('should navigate through all tabs', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // 验证初始页面
      expect(find.text('香薰养护记录器'), findsOneWidget);

      // 点击使用记录标签
      await tester.tap(find.text('使用记录'));
      await tester.pumpAndSettle();
      expect(find.text('使用记录'), findsOneWidget);

      // 点击养护记录标签
      await tester.tap(find.text('养护记录'));
      await tester.pumpAndSettle();
      expect(find.text('养护记录'), findsOneWidget);

      // 点击统计标签
      await tester.tap(find.text('统计'));
      await tester.pumpAndSettle();
      expect(find.text('统计分析'), findsOneWidget);
    });

    testWidgets('should search and filter aromas', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // 点击搜索按钮
      await tester.tap(find.byIcon(Icons.search));
      await tester.pumpAndSettle();

      // 输入搜索内容
      await tester.enterText(find.byType(TextField), '薰衣草');
      await tester.tap(find.text('搜索'));
      await tester.pumpAndSettle();

      // 验证搜索结果
      expect(find.text('搜索: 薰衣草'), findsOneWidget);
    });
  });
}

部署发布

1. Android打包

flutter build apk --release

2. iOS打包

flutter build ios --release

3. 应用商店发布

准备应用图标、截图和描述,提交到各大应用商店。

总结

本教程详细介绍了Flutter香薰养护记录器应用的完整开发过程,涵盖了:

  • 数据模型设计:完整的香薰信息、使用记录、养护记录模型
  • UI界面开发:Material Design 3风格的现代化界面
  • 功能实现:收藏管理、使用追踪、养护记录、统计分析
  • 智能监控:库存状态、过期提醒、使用频率分析
  • 搜索筛选:多维度搜索和智能筛选功能
  • 动画效果:提升用户体验的淡入动画
  • 性能优化:列表优化、状态管理、内存管理
  • 扩展功能:数据持久化、图片上传、通知提醒
  • 测试策略:单元测试、Widget测试、集成测试

这款应用不仅功能完整,而且代码结构清晰,易于维护和扩展。通过本教程的学习,你可以掌握Flutter生活记录类应用开发的核心技能,为后续开发更复杂的个人管理应用打下坚实基础。

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

Logo

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

更多推荐