Flutter 框架跨平台鸿蒙开发 - 居家香薰摆放记录应用开发教程
Flutter居家香薰摆放记录应用是一个功能全面的家居管理工具,涵盖了香薰摆放记录、房间管理、库存监控和效果分析等核心功能。完整的数据模型设计:从香薰摆放记录到效果分析的全面数据结构丰富的UI组件实现:卡片式布局、状态指示器、进度条等现代化界面智能的筛选和搜索:多维度数据筛选和实时搜索功能直观的统计分析:使用数据的可视化展示和分析流畅的动画效果:页面切换和缩放动画的实现可扩展的架构设计:支持数据持
·
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来区分使用中和已移除的记录 intensity和effectRating使用双精度浮点数支持精确评分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居家香薰摆放记录应用是一个功能全面的家居管理工具,涵盖了香薰摆放记录、房间管理、库存监控和效果分析等核心功能。通过本教程,你学习了:
- 完整的数据模型设计:从香薰摆放记录到效果分析的全面数据结构
- 丰富的UI组件实现:卡片式布局、状态指示器、进度条等现代化界面
- 智能的筛选和搜索:多维度数据筛选和实时搜索功能
- 直观的统计分析:使用数据的可视化展示和分析
- 流畅的动画效果:页面切换和缩放动画的实现
- 可扩展的架构设计:支持数据持久化、图片管理、通知提醒等功能扩展
这个应用不仅适用于香薰管理,其设计模式和实现方法也可以应用到其他家居用品管理、生活记录等相关领域。通过学习本教程,你掌握了Flutter应用开发的核心技能,能够独立开发类似的生活管理应用。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)