Flutter居家香薰摆放记录应用开发教程

项目简介

居家香薰摆放记录应用是一款专为香薰爱好者设计的家居管理工具,帮助用户科学记录和管理家中各个区域的香薰摆放情况。应用涵盖了香薰摆放记录、房间管理、库存管理和效果分析等功能,让用户能够更好地享受香薰带来的美好生活体验。

运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

核心功能特性

  • 摆放记录管理:详细记录每个香薰的摆放位置、使用时间和效果评价
  • 房间信息管理:管理家中各个房间的香薰使用情况和偏好设置
  • 库存管理系统:追踪香薰库存状态,提供过期和库存不足提醒
  • 效果分析统计:分析香薰使用效果,提供数据洞察和使用建议
  • 多维度筛选:支持按房间、状态、香味等条件筛选记录
  • 智能提醒功能:库存不足、即将过期等智能提醒
  • 视觉化统计:直观的图表展示使用习惯和偏好分析
  • 流畅动画效果:优雅的界面动画,提升用户体验

技术架构

开发环境

  • 框架:Flutter 3.x
  • 开发语言:Dart
  • UI组件:Material Design 3
  • 状态管理:StatefulWidget
  • 数据存储:内存存储(可扩展为本地数据库)

项目结构

lib/
├── main.dart                 # 应用入口和主要逻辑
├── models/                   # 数据模型(本教程中集成在main.dart)
│   ├── aroma_placement_record.dart
│   ├── room_info.dart
│   ├── aroma_inventory.dart
│   └── effect_record.dart
├── pages/                    # 页面组件
│   ├── placement_records_page.dart
│   ├── rooms_page.dart
│   ├── inventory_page.dart
│   └── effect_analysis_page.dart
└── widgets/                  # 自定义组件
    ├── placement_record_card.dart
    ├── room_card.dart
    └── inventory_card.dart

数据模型设计

1. 香薰摆放记录模型(AromaPlacementRecord)

class AromaPlacementRecord {
  final String id;              // 记录ID
  final String aromaName;       // 香薰名称
  final String aromaType;       // 香薰类型:精油、香薰蜡烛、香薰石等
  final String scent;           // 香味:薰衣草、柠檬、玫瑰等
  final String roomName;        // 房间名称
  final String specificLocation; // 具体位置:床头柜、客厅茶几等
  final DateTime placementDate; // 摆放日期
  final DateTime? removalDate;  // 移除日期
  final String status;          // 状态:使用中、已移除、需更换
  final int durationHours;      // 使用时长(小时)
  final double intensity;       // 香味强度(1-5)
  final String purpose;         // 使用目的:助眠、提神、净化空气等
  final String notes;           // 备注
  final List<String> photos;    // 摆放照片
  final double effectRating;    // 效果评分(1-5)
  final String moodImpact;      // 心情影响:放松、愉悦、平静等
}

设计要点

  • 使用可选的removalDate来区分使用中和已移除的记录
  • intensityeffectRating使用双精度浮点数支持精确评分
  • photos列表存储摆放过程的图片路径
  • moodImpact记录香薰对心情的具体影响

2. 房间信息模型(RoomInfo)

class RoomInfo {
  final String id;              // 房间ID
  final String roomName;        // 房间名称
  final String roomType;        // 房间类型:卧室、客厅、书房等
  final double area;            // 房间面积(平方米)
  final String ventilation;     // 通风情况:良好、一般、较差
  final List<String> currentAromas; // 当前摆放的香薰ID列表
  final String preferredScents; // 偏好香味
  final String notes;           // 房间备注
}

设计要点

  • area字段帮助判断香薰用量和摆放密度
  • ventilation影响香薰扩散效果和使用建议
  • currentAromas实时追踪房间内的香薰状态
  • preferredScents记录房间的香味偏好

3. 香薰库存模型(AromaInventory)

class AromaInventory {
  final String id;              // 库存ID
  final String aromaName;       // 香薰名称
  final String aromaType;       // 香薰类型
  final String scent;           // 香味
  final String brand;           // 品牌
  final DateTime purchaseDate;  // 购买日期
  final double price;           // 价格
  final int quantity;           // 数量
  final int usedCount;          // 已使用次数
  final String storageLocation; // 存放位置
  final DateTime? expiryDate;   // 过期日期
  final String notes;           // 备注
}

设计要点

  • 记录购买信息便于成本分析
  • usedCount追踪使用频率
  • expiryDate支持过期提醒功能
  • storageLocation帮助快速定位香薰

4. 使用效果记录模型(EffectRecord)

class EffectRecord {
  final String id;              // 效果记录ID
  final String placementRecordId; // 关联的摆放记录ID
  final DateTime recordDate;    // 记录日期
  final String timeOfDay;       // 时间段:早晨、下午、晚上
  final double moodBefore;      // 使用前心情评分(1-5)
  final double moodAfter;       // 使用后心情评分(1-5)
  final String physicalEffect; // 身体感受:放松、提神、困倦等
  final String mentalEffect;   // 心理感受:平静、愉悦、专注等
  final int durationMinutes;   // 感受持续时间(分钟)
  final String notes;          // 详细感受记录
}

设计要点

  • 通过placementRecordId关联具体的摆放记录
  • 记录使用前后的心情变化
  • 区分身体和心理两个层面的效果
  • 记录效果持续时间便于分析

核心功能实现

1. 摆放记录管理

记录卡片展示
Widget _buildPlacementRecordCard(AromaPlacementRecord record) {
  final daysSincePlacement = DateTime.now().difference(record.placementDate).inDays;

  return Card(
    elevation: 4,
    margin: const EdgeInsets.only(bottom: 16),
    child: InkWell(
      onTap: () => _showPlacementRecordDetail(record),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 状态指示器和标题
            Row(
              children: [
                Container(
                  width: 12,
                  height: 12,
                  decoration: BoxDecoration(
                    color: _getStatusColor(record.status),
                    shape: BoxShape.circle,
                  ),
                ),
                SizedBox(width: 8),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(record.aromaName, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                      Text('${record.aromaType}${record.scent}'),
                    ],
                  ),
                ),
                Container(
                  padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  decoration: BoxDecoration(
                    color: _getStatusColor(record.status),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(record.status, style: TextStyle(color: Colors.white)),
                ),
              ],
            ),
            
            // 位置信息展示
            Container(
              padding: EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.purple.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(8),
                border: Border.all(color: Colors.purple.withValues(alpha: 0.3)),
              ),
              child: Row(
                children: [
                  Icon(Icons.location_on, color: Colors.purple.shade600),
                  SizedBox(width: 8),
                  Text('${record.roomName} - ${record.specificLocation}'),
                ],
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

实现要点

  • 使用圆形状态指示器直观显示使用状态
  • 位置信息使用特殊样式突出显示
  • 支持点击查看详细信息
  • 显示摆放天数便于时间管理
多维度筛选功能
List<AromaPlacementRecord> _getFilteredPlacementRecords() {
  return _placementRecords.where((record) {
    // 搜索过滤
    if (_searchQuery.isNotEmpty) {
      final query = _searchQuery.toLowerCase();
      if (!record.aromaName.toLowerCase().contains(query) &&
          !record.scent.toLowerCase().contains(query) &&
          !record.roomName.toLowerCase().contains(query) &&
          !record.specificLocation.toLowerCase().contains(query)) {
        return false;
      }
    }

    // 房间过滤
    if (_selectedRoom != null && record.roomName != _selectedRoom) {
      return false;
    }

    // 状态过滤
    if (_selectedStatus != null && record.status != _selectedStatus) {
      return false;
    }

    // 香味过滤
    if (_selectedScent != null && record.scent != _selectedScent) {
      return false;
    }

    // 仅使用中过滤
    if (_showActiveOnly && record.status != '使用中') {
      return false;
    }

    return true;
  }).toList()..sort((a, b) => b.placementDate.compareTo(a.placementDate));
}

实现要点

  • 支持文本搜索(香薰名称、香味、房间、位置)
  • 多个筛选条件可以组合使用
  • 结果按摆放日期倒序排列
  • 实时筛选,无需刷新页面

2. 房间管理系统

房间信息展示
Widget _buildRoomCard(RoomInfo room) {
  final currentAromaCount = room.currentAromas.length;
  final roomRecords = _placementRecords
      .where((record) => record.roomName == room.roomName && record.status == '使用中')
      .toList();

  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          Row(
            children: [
              Container(
                padding: EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: Colors.purple.withValues(alpha: 0.1),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Icon(_getRoomIcon(room.roomType), color: Colors.purple.shade600),
              ),
              SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(room.roomName, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                    Text('${room.roomType}${room.area}㎡'),
                  ],
                ),
              ),
              Container(
                padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                decoration: BoxDecoration(
                  color: currentAromaCount > 0 ? Colors.green : Colors.grey,
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Text('$currentAromaCount个香薰', style: TextStyle(color: Colors.white)),
              ),
            ],
          ),
        ],
      ),
    ),
  );
}

实现要点

  • 使用图标区分不同房间类型
  • 实时显示房间内香薰数量
  • 展示房间面积和通风情况
  • 列出当前使用的香薰详情
房间图标映射
IconData _getRoomIcon(String roomType) {
  switch (roomType) {
    case '卧室': return Icons.bed;
    case '客厅': return Icons.weekend;
    case '书房': return Icons.menu_book;
    case '厨房': return Icons.kitchen;
    case '浴室': return Icons.bathtub;
    case '阳台': return Icons.balcony;
    case '餐厅': return Icons.dining;
    default: return Icons.home;
  }
}

3. 库存管理功能

库存状态监控
Widget _buildInventoryCard(AromaInventory item) {
  final isLowStock = item.quantity <= 1;
  final isExpiringSoon = item.expiryDate != null &&
      item.expiryDate!.difference(DateTime.now()).inDays <= 30;

  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(item.aromaName, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                    Text('${item.aromaType}${item.scent}${item.brand}'),
                  ],
                ),
              ),
              Column(
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  Text(
                    '${item.quantity}个',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                      color: isLowStock ? Colors.red : Colors.green,
                    ),
                  ),
                  if (isLowStock)
                    Container(
                      padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                      decoration: BoxDecoration(
                        color: Colors.red,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Text('库存不足', style: TextStyle(color: Colors.white, fontSize: 10)),
                    ),
                ],
              ),
            ],
          ),
          
          // 即将过期警告
          if (isExpiringSoon)
            Container(
              padding: EdgeInsets.all(8),
              decoration: BoxDecoration(
                color: Colors.red.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(6),
                border: Border.all(color: Colors.red.withValues(alpha: 0.3)),
              ),
              child: Row(
                children: [
                  Icon(Icons.warning, color: Colors.red.shade600, size: 16),
                  SizedBox(width: 8),
                  Text('即将过期,请尽快使用', style: TextStyle(color: Colors.red.shade600)),
                ],
              ),
            ),
        ],
      ),
    ),
  );
}

实现要点

  • 自动检测库存不足(数量≤1)
  • 检测即将过期的香薰(30天内)
  • 使用颜色和标签进行状态提醒
  • 记录使用次数和成本信息

4. 效果分析统计

总体统计
Widget _buildOverallStatsCard() {
  final totalRecords = _placementRecords.length;
  final activeRecords = _placementRecords.where((r) => r.status == '使用中').length;
  final totalHours = _placementRecords.fold(0, (sum, record) => sum + record.durationHours);
  final avgRating = _placementRecords.isNotEmpty
      ? _placementRecords.fold(0.0, (sum, record) => sum + record.effectRating) / totalRecords
      : 0.0;

  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          Row(
            children: [
              Expanded(child: _buildStatItem('总记录', '$totalRecords个', Icons.assignment, Colors.blue)),
              Expanded(child: _buildStatItem('使用中', '$activeRecords个', Icons.play_circle, Colors.green)),
            ],
          ),
          SizedBox(height: 16),
          Row(
            children: [
              Expanded(child: _buildStatItem('总时长', '${totalHours}小时', Icons.schedule, Colors.orange)),
              Expanded(child: _buildStatItem('平均评分', '${avgRating.toStringAsFixed(1)}分', Icons.star, Colors.purple)),
            ],
          ),
        ],
      ),
    ),
  );
}

实现要点

  • 统计总记录数和使用中的数量
  • 计算总使用时长和平均效果评分
  • 使用图标和颜色区分不同指标
  • 数据实时更新
房间使用分布统计
Widget _buildRoomUsageCard() {
  final roomStats = <String, int>{};
  for (final record in _placementRecords) {
    roomStats[record.roomName] = (roomStats[record.roomName] ?? 0) + 1;
  }

  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: roomStats.entries.map((entry) {
          final percentage = entry.value / _placementRecords.length;
          return Column(
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(entry.key),
                  Text('${entry.value}次 (${(percentage * 100).toStringAsFixed(1)}%)'),
                ],
              ),
              LinearProgressIndicator(
                value: percentage,
                valueColor: AlwaysStoppedAnimation<Color>(Colors.purple.shade600),
              ),
            ],
          );
        }).toList(),
      ),
    ),
  );
}

实现要点

  • 统计各房间的香薰使用次数
  • 计算使用比例并可视化展示
  • 使用进度条直观显示分布情况

动画效果实现

1. 页面切换动画

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,
  ));

  _scaleAnimationController = AnimationController(
    duration: const Duration(milliseconds: 600),
    vsync: this,
  );

  _scaleAnimation = Tween<double>(
    begin: 0.8,
    end: 1.0,
  ).animate(CurvedAnimation(
    parent: _scaleAnimationController,
    curve: Curves.elasticOut,
  ));

  _fadeAnimationController.forward();
  _scaleAnimationController.forward();
}

// 在build方法中使用
FadeTransition(
  opacity: _fadeAnimation,
  child: ScaleTransition(
    scale: _scaleAnimation,
    child: IndexedStack(
      index: _selectedIndex,
      children: [
        _buildPlacementRecordsPage(),
        _buildRoomsPage(),
        _buildInventoryPage(),
        _buildEffectAnalysisPage(),
      ],
    ),
  ),
)

动画特点

  • 淡入淡出效果让页面切换更平滑
  • 弹性缩放动画增加趣味性
  • 使用不同的缓动曲线增强视觉效果
  • 动画时长经过优化,不会影响用户体验

用户界面设计

1. 色彩方案

theme: ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
  useMaterial3: true,
),

设计理念

  • 主色调使用紫色,符合香薰的优雅氛围
  • 采用Material Design 3设计规范
  • 状态颜色:绿色(使用中)、灰色(已移除)、橙色(需更换)

2. 信息层次设计

Widget _buildInfoItem(IconData icon, String label, String value, Color color) {
  return Column(
    children: [
      Icon(icon, color: color, size: 20),
      SizedBox(height: 4),
      Text(label, style: TextStyle(fontSize: 10, color: Colors.grey.shade600)),
      SizedBox(height: 2),
      Text(value, style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: color)),
    ],
  );
}

设计原则

  • 图标+标签+数值的三层信息结构
  • 使用颜色区分不同类型的信息
  • 字体大小层次分明,便于阅读

3. 卡片设计

Card(
  elevation: 4,
  margin: const EdgeInsets.only(bottom: 16),
  child: InkWell(
    onTap: () => _showPlacementRecordDetail(record),
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: // 卡片内容
    ),
  ),
)

设计要点

  • 使用阴影增强层次感
  • 支持点击交互反馈
  • 统一的内边距和外边距
  • 圆角设计更加现代

数据管理策略

1. 示例数据初始化

void _initializeData() {
  // 初始化房间信息
  _rooms = [
    RoomInfo(
      id: '1',
      roomName: '主卧室',
      roomType: '卧室',
      area: 20.0,
      ventilation: '良好',
      currentAromas: ['1', '3'],
      preferredScents: '薰衣草、洋甘菊',
      notes: '主要用于睡眠,偏好舒缓香味',
    ),
    // 更多房间数据...
  ];

  // 初始化摆放记录
  _placementRecords = [
    AromaPlacementRecord(
      id: '1',
      aromaName: '薰衣草精油',
      aromaType: '精油',
      scent: '薰衣草',
      roomName: '主卧室',
      specificLocation: '床头柜',
      placementDate: DateTime.now().subtract(const Duration(days: 7)),
      removalDate: null,
      status: '使用中',
      durationHours: 168, // 7天
      intensity: 3.0,
      purpose: '助眠',
      notes: '每晚睡前使用,效果很好',
      photos: ['bedroom_lavender_1.jpg', 'bedroom_lavender_2.jpg'],
      effectRating: 4.5,
      moodImpact: '放松',
    ),
    // 更多记录数据...
  ];
}

2. 筛选对话框实现

void _showFilterDialog() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('筛选记录'),
      content: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('房间:'),
            Wrap(
              spacing: 8,
              children: [
                FilterChip(
                  label: Text('全部'),
                  selected: _selectedRoom == null,
                  onSelected: (selected) {
                    setState(() {
                      _selectedRoom = selected ? null : _selectedRoom;
                    });
                  },
                ),
                ..._rooms.map((room) => FilterChip(
                      label: Text(room.roomName),
                      selected: _selectedRoom == room.roomName,
                      onSelected: (selected) {
                        setState(() {
                          _selectedRoom = selected ? room.roomName : null;
                        });
                      },
                    )),
              ],
            ),
            // 更多筛选选项...
          ],
        ),
      ),
      actions: [
        TextButton(onPressed: () => Navigator.of(context).pop(), child: Text('取消')),
        ElevatedButton(
          onPressed: () {
            Navigator.of(context).pop();
            setState(() {});
          },
          child: Text('应用'),
        ),
      ],
    ),
  );
}

扩展功能建议

1. 数据持久化

// 使用SharedPreferences存储数据
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class DataManager {
  static const String _placementRecordsKey = 'placement_records';
  static const String _roomsKey = 'rooms';
  static const String _inventoryKey = 'inventory';

  static Future<void> savePlacementRecords(List<AromaPlacementRecord> records) async {
    final prefs = await SharedPreferences.getInstance();
    final recordsJson = records.map((r) => r.toJson()).toList();
    await prefs.setString(_placementRecordsKey, json.encode(recordsJson));
  }

  static Future<List<AromaPlacementRecord>> loadPlacementRecords() async {
    final prefs = await SharedPreferences.getInstance();
    final recordsString = prefs.getString(_placementRecordsKey);
    if (recordsString != null) {
      final recordsJson = json.decode(recordsString) as List;
      return recordsJson.map((json) => AromaPlacementRecord.fromJson(json)).toList();
    }
    return [];
  }
}

2. 图片管理

// 使用image_picker选择和管理图片
import 'package:image_picker/image_picker.dart';
import 'dart:io';

class ImageManager {
  static final ImagePicker _picker = ImagePicker();

  static Future<String?> pickImage() async {
    final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
    return image?.path;
  }

  static Future<List<String>> pickMultipleImages() async {
    final List<XFile> images = await _picker.pickMultiImage();
    return images.map((image) => image.path).toList();
  }

  static Widget buildImageThumbnail(String imagePath) {
    return Container(
      width: 60,
      height: 60,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(8),
        image: DecorationImage(
          image: FileImage(File(imagePath)),
          fit: BoxFit.cover,
        ),
      ),
    );
  }
}

3. 通知提醒系统

// 使用flutter_local_notifications实现提醒
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

class NotificationManager {
  static final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();

  static Future<void> initialize() async {
    const AndroidInitializationSettings initializationSettingsAndroid = 
        AndroidInitializationSettings('@mipmap/ic_launcher');
    
    const InitializationSettings initializationSettings = InitializationSettings(
      android: initializationSettingsAndroid,
    );
    
    await _notifications.initialize(initializationSettings);
  }

  static Future<void> scheduleExpiryReminder(AromaInventory item) async {
    if (item.expiryDate != null) {
      final reminderDate = item.expiryDate!.subtract(const Duration(days: 7));
      
      await _notifications.schedule(
        item.id.hashCode,
        '香薰即将过期',
        '${item.aromaName} 将在一周后过期,请尽快使用',
        reminderDate,
        const NotificationDetails(
          android: AndroidNotificationDetails(
            'aroma_expiry_channel',
            '香薰过期提醒',
            channelDescription: '提醒用户香薰即将过期',
            importance: Importance.max,
            priority: Priority.high,
          ),
        ),
      );
    }
  }

  static Future<void> scheduleLowStockReminder(AromaInventory item) async {
    if (item.quantity <= 1) {
      await _notifications.show(
        item.id.hashCode + 1000,
        '香薰库存不足',
        '${item.aromaName} 库存不足,建议及时补充',
        const NotificationDetails(
          android: AndroidNotificationDetails(
            'aroma_stock_channel',
            '香薰库存提醒',
            channelDescription: '提醒用户香薰库存不足',
            importance: Importance.high,
            priority: Priority.high,
          ),
        ),
      );
    }
  }
}

4. 数据导出功能

// 导出使用报告
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

class ExportManager {
  static Future<void> exportUsageReport(
    List<AromaPlacementRecord> records,
    List<RoomInfo> rooms,
  ) async {
    final pdf = pw.Document();
    
    pdf.addPage(
      pw.Page(
        build: (pw.Context context) {
          return pw.Column(
            crossAxisAlignment: pw.CrossAxisAlignment.start,
            children: [
              pw.Text('居家香薰使用报告', style: pw.TextStyle(fontSize: 24, fontWeight: pw.FontWeight.bold)),
              pw.SizedBox(height: 20),
              
              // 总体统计
              pw.Text('总体统计', style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold)),
              pw.Text('总记录数: ${records.length}'),
              pw.Text('使用中: ${records.where((r) => r.status == '使用中').length}'),
              pw.Text('平均评分: ${records.fold(0.0, (sum, r) => sum + r.effectRating) / records.length}'),
              
              pw.SizedBox(height: 20),
              
              // 房间分布
              pw.Text('房间使用分布', style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold)),
              ...rooms.map((room) {
                final roomRecords = records.where((r) => r.roomName == room.roomName).length;
                return pw.Text('${room.roomName}: $roomRecords次');
              }),
              
              pw.SizedBox(height: 20),
              
              // 详细记录
              pw.Text('详细记录', style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold)),
              ...records.take(10).map((record) => pw.Column(
                crossAxisAlignment: pw.CrossAxisAlignment.start,
                children: [
                  pw.Text('${record.aromaName} - ${record.roomName}'),
                  pw.Text('状态: ${record.status}, 评分: ${record.effectRating}'),
                  pw.SizedBox(height: 10),
                ],
              )),
            ],
          );
        },
      ),
    );
    
    // 保存PDF文件
    // final output = await getTemporaryDirectory();
    // final file = File("${output.path}/aroma_usage_report.pdf");
    // await file.writeAsBytes(await pdf.save());
  }
}

5. 智能推荐系统

class RecommendationEngine {
  static List<String> recommendAromasForRoom(
    RoomInfo room,
    List<AromaPlacementRecord> records,
    List<AromaInventory> inventory,
  ) {
    final recommendations = <String>[];
    
    // 基于房间类型推荐
    switch (room.roomType) {
      case '卧室':
        recommendations.addAll(['薰衣草', '洋甘菊', '檀香']);
        break;
      case '客厅':
        recommendations.addAll(['柠檬', '薄荷', '尤加利']);
        break;
      case '书房':
        recommendations.addAll(['迷迭香', '薄荷', '柠檬']);
        break;
      case '浴室':
        recommendations.addAll(['尤加利', '茶树', '薄荷']);
        break;
    }
    
    // 基于历史使用记录推荐
    final roomRecords = records.where((r) => r.roomName == room.roomName).toList();
    final highRatedScents = roomRecords
        .where((r) => r.effectRating >= 4.0)
        .map((r) => r.scent)
        .toSet()
        .toList();
    
    recommendations.addAll(highRatedScents);
    
    // 过滤库存中有的香薰
    final availableScents = inventory
        .where((item) => item.quantity > 0)
        .map((item) => item.scent)
        .toSet();
    
    return recommendations
        .where((scent) => availableScents.contains(scent))
        .toSet()
        .take(5)
        .toList();
  }

  static String recommendOptimalPlacement(
    String aromaType,
    RoomInfo room,
  ) {
    switch (aromaType) {
      case '精油':
        return room.roomType == '卧室' ? '床头柜' : '茶几';
      case '香薰蜡烛':
        return '安全的平面位置';
      case '香薰石':
        return '通风良好的角落';
      default:
        return '房间中央位置';
    }
  }
}

性能优化建议

1. 列表优化

// 使用ListView.builder进行懒加载
ListView.builder(
  itemCount: filteredRecords.length,
  itemBuilder: (context, index) {
    final record = filteredRecords[index];
    return _buildPlacementRecordCard(record);
  },
)

// 对于大量数据,可以使用分页加载
class PaginatedRecordsList extends StatefulWidget {
  
  _PaginatedRecordsListState createState() => _PaginatedRecordsListState();
}

class _PaginatedRecordsListState extends State<PaginatedRecordsList> {
  final ScrollController _scrollController = ScrollController();
  List<AromaPlacementRecord> _displayedRecords = [];
  int _currentPage = 0;
  final int _pageSize = 20;

  
  void initState() {
    super.initState();
    _loadMoreRecords();
    _scrollController.addListener(_onScroll);
  }

  void _onScroll() {
    if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
      _loadMoreRecords();
    }
  }

  void _loadMoreRecords() {
    final startIndex = _currentPage * _pageSize;
    final newRecords = _allRecords.skip(startIndex).take(_pageSize).toList();
    
    setState(() {
      _displayedRecords.addAll(newRecords);
      _currentPage++;
    });
  }
}

2. 状态管理优化

// 使用Provider进行状态管理
import 'package:provider/provider.dart';

class AromaProvider extends ChangeNotifier {
  List<AromaPlacementRecord> _placementRecords = [];
  List<RoomInfo> _rooms = [];
  List<AromaInventory> _inventory = [];
  
  String _searchQuery = '';
  String? _selectedRoom;
  
  List<AromaPlacementRecord> get filteredRecords {
    return _placementRecords.where((record) {
      if (_searchQuery.isNotEmpty) {
        final query = _searchQuery.toLowerCase();
        if (!record.aromaName.toLowerCase().contains(query)) {
          return false;
        }
      }
      if (_selectedRoom != null && record.roomName != _selectedRoom) {
        return false;
      }
      return true;
    }).toList();
  }
  
  void updateSearchQuery(String query) {
    _searchQuery = query;
    notifyListeners();
  }
  
  void updateRoomFilter(String? room) {
    _selectedRoom = room;
    notifyListeners();
  }
  
  void addPlacementRecord(AromaPlacementRecord record) {
    _placementRecords.add(record);
    notifyListeners();
  }
}

测试策略

1. 单元测试

// test/models/aroma_placement_record_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app/models/aroma_placement_record.dart';

void main() {
  group('AromaPlacementRecord', () {
    test('should calculate duration correctly', () {
      final record = AromaPlacementRecord(
        id: '1',
        aromaName: 'Test Aroma',
        placementDate: DateTime.now().subtract(Duration(days: 7)),
        removalDate: DateTime.now(),
        // ... 其他必需字段
      );
      
      final duration = record.calculateDuration();
      expect(duration.inDays, equals(7));
    });
    
    test('should identify active records', () {
      final record = AromaPlacementRecord(
        id: '1',
        status: '使用中',
        removalDate: null,
        // ... 其他字段
      );
      
      expect(record.isActive, isTrue);
    });
  });
}

2. Widget测试

// test/widgets/placement_record_card_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app/widgets/placement_record_card.dart';

void main() {
  group('PlacementRecordCard', () {
    testWidgets('should display record information correctly', (WidgetTester tester) async {
      final record = AromaPlacementRecord(
        id: '1',
        aromaName: 'Test Aroma',
        scent: 'Test Scent',
        roomName: 'Test Room',
        status: '使用中',
        // ... 其他字段
      );
      
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: PlacementRecordCard(record: record),
          ),
        ),
      );
      
      expect(find.text('Test Aroma'), findsOneWidget);
      expect(find.text('Test Scent'), findsOneWidget);
      expect(find.text('Test Room'), findsOneWidget);
      expect(find.text('使用中'), findsOneWidget);
    });
    
    testWidgets('should show status indicator', (WidgetTester tester) async {
      final record = AromaPlacementRecord(
        // ... 字段设置
        status: '使用中',
      );
      
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: PlacementRecordCard(record: record),
          ),
        ),
      );
      
      expect(find.byType(Container), 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:your_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  group('App Integration Tests', () {
    testWidgets('should navigate between tabs', (WidgetTester 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 filter records correctly', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();
      
      // 打开筛选对话框
      await tester.tap(find.byIcon(Icons.filter_list));
      await tester.pumpAndSettle();
      
      // 选择房间筛选
      await tester.tap(find.text('主卧室'));
      await tester.pumpAndSettle();
      
      await tester.tap(find.text('应用'));
      await tester.pumpAndSettle();
      
      // 验证筛选结果
      expect(find.byType(Card), findsWidgets);
    });
  });
}

部署和发布

1. Android打包

# 生成签名密钥
keytool -genkey -v -keystore ~/aroma-placement-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias aroma-placement

# 配置android/app/build.gradle
android {
    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword keystoreProperties['storePassword']
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

# 构建APK
flutter build apk --release

2. iOS打包

# 构建iOS应用
flutter build ios --release

# 使用Xcode进行最终打包和发布
open ios/Runner.xcworkspace

总结

Flutter居家香薰摆放记录应用是一个功能全面的家居管理工具,涵盖了香薰摆放记录、房间管理、库存监控和效果分析等核心功能。通过本教程,你学习了:

  1. 完整的数据模型设计:从香薰摆放记录到效果分析的全面数据结构
  2. 丰富的UI组件实现:卡片式布局、状态指示器、进度条等现代化界面
  3. 智能的筛选和搜索:多维度数据筛选和实时搜索功能
  4. 直观的统计分析:使用数据的可视化展示和分析
  5. 流畅的动画效果:页面切换和缩放动画的实现
  6. 可扩展的架构设计:支持数据持久化、图片管理、通知提醒等功能扩展

这个应用不仅适用于香薰管理,其设计模式和实现方法也可以应用到其他家居用品管理、生活记录等相关领域。通过学习本教程,你掌握了Flutter应用开发的核心技能,能够独立开发类似的生活管理应用。

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

Logo

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

更多推荐