Flutter 框架跨平台鸿蒙开发 - 香薰养护记录器应用开发教程
essentialOil, // 精油candle, // 蜡烛diffuserStone, // 扩香石reedDiffuser, // 藤条扩香incense, // 香薰potpourri, // 干花香薰数据模型设计:完整的香薰信息、使用记录、养护记录模型UI界面开发:Material Design 3风格的现代化界面功能实现智能监控:库存状态、过期提醒、使用频率分析搜索筛选:多维度搜索和
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, // 甜香调
}
页面架构
应用采用底部导航栏设计,包含四个主要页面:
- 香薰页面:展示所有香薰收藏,支持搜索和筛选
- 使用记录页面:记录和查看香薰使用历史
- 养护记录页面:记录香薰的清洁和维护操作
- 统计页面:展示使用统计和数据分析
详细实现步骤
第一步:项目初始化
创建新的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
更多推荐



所有评论(0)