Flutter for OpenHarmony 植物养护 App:用数字花园培育你的绿色生活
在快节奏的都市生活中,一盆绿植不仅是窗台的点缀,更是心灵的慰藉。然而,"忘记浇水" 或 "过度溺爱" 常常让我们的植物伙伴悄然枯萎。
🌐 加入社区 欢迎加入 开源鸿蒙跨平台开发者社区 ,获取最新资源与技术支持: 👉 开源鸿蒙跨平台开发者社区
完整效果


一、核心理念:让养护变得智能而有温度
该 App 围绕三个关键体验构建:
- 智能提醒:基于浇水间隔自动判断是否"口渴";
- 可视化成长:植物图标随生长阶段变化,浇水可能触发"成长动画";
- 情感反馈:健康状态文字("茁壮成长"、"需浇水")+ 进度条颜色变化,赋予植物"情绪"。
🌿 技术不应冰冷,而应成为连接人与自然的桥梁。
二、数据模型:会"思考"的 Plant 类
dart
class Plant {
final String name;
final String type; // 观叶/多肉/开花
final int waterInterval; // 浇水周期(天)
final DateTime lastWatered;
final DateTime plantedDate;
final double growthStage; // 0.0(幼苗)→ 1.0(成熟)
bool get needsWater { ... } // 是否需要浇水?
String get healthStatus { ... } // 健康状态描述
IconData get plantIcon { ... } // 动态图标
}

- 时间感知 :通过
DateTime.now().difference(lastWatered)计算"口渴程度"; - 类型差异化 :
- 多肉:耐旱(浇水间隔长达14天);
- 开花植物:需频繁浇水(2-3天);
- 生长系统 :
growthStage模拟真实植物生命周期。
💡 浇水 ≠ 机械操作:每次浇水有50%概率促进生长,模拟"精心照料带来回报"的自然法则。
三、交互亮点:让每一次点击都有意义
1. 动态植物图标 + 旋转动画
dart
AnimatedSwitcher(
transitionBuilder: (child, animation) => RotationTransition(...),
child: Icon(plant.plantIcon, key: ValueKey(plant.growthStage)),
)

- 当
growthStage变化时,图标自动切换(🌱 → 🌿 → 🌸); - 切换时伴随顺时针旋转动画,象征"焕发生机";
- 不同类型使用不同图标:
local_florist(开花/多肉)、eco(观叶)。
2. 智能浇水按钮
- 常态:绿色"浇水"按钮;
- 缺水时:变为红色"立即浇水",视觉警示;
- 点击后弹出 SnackBar:"小绿已浇水!已成熟!"------即时正向反馈。
3. 浇水进度条:时间的可视化
dart
LinearProgressIndicator(
value: daysSinceWater / waterInterval,
color: needsWater ? Colors.red : Colors.green,
)

- 进度从绿渐变至红,直观展示"距离下次浇水还有多久";
- 超过100%时保持红色,强调"急需关注"。
4. 撤销删除(Undo Pattern)
- 删除植物后,Snackbar 提供"撤销"选项;
- 避免误删心爱的"仙人球",体现人性化设计。
四、细节巧思:超越功能的情感化设计
✅ 随机生成新植物
dart
final plantNames = ['薄荷', '绿萝', '芦荟', ...];
final types = ['观叶', '多肉', '开花'];
// 自动分配合理浇水间隔
- 点击 FAB 按钮,随机添加一株真实存在的植物;
- 自动匹配其类型对应的浇水频率(如多肉=14天,开花=2天);
- 降低使用门槛,让用户立刻进入"园丁"角色。
✅ 养护小贴士(Help Dialog)
- 点击 AppBar 的
info_outline图标; - 弹出针对不同类型植物 的养护建议:
- "观叶:避免阳光直射"
- "多肉:宁干勿湿"
- "开花:花期需磷钾肥"
- 底部特别提示:"进度条变红时,请及时浇水!"------强化核心交互逻辑。
✅ 健康状态语义化
| 状态 | 条件 | 用户感受 |
|---|---|---|
| 需浇水 | 超过浇水间隔 | 紧迫感,需立即行动 |
| 状态良好 | 距离下次浇水 > 50% | 安心,维持现状 |
| 茁壮成长 | 刚浇完水 | 成就感,被认可 |
🌱 文字比颜色更能传递情感------"茁壮成长"比"健康"更有生命力。
五、UI/UX 设计:清新治愈的植物美学
| 元素 | 设计说明 |
|---|---|
| 主色调 | Colors.green + 深绿 AppBar (#2E7D32) ------ 自然、专业 |
| 背景色 | #FCFDFC ------ 接近纸张的米白,柔和不刺眼 |
| 卡片圆角 | 20 ------ 比常规更大,模拟"花盆"轮廓 |
| 空状态 | 绿色描边圆形 + local_florist 图标,引导明确 |
| FAB 按钮 | 绿底白加号,符合 Material 规范 |
🎨 整体风格宁静、有机、无干扰,让用户专注于植物本身。
六、技术实现亮点
| 技术点 | 应用说明 |
|---|---|
AnimatedSwitcher + RotationTransition |
实现图标切换的流畅旋转动画 |
ValueKey(plant.growthStage) |
确保动画在 growthStage 变化时触发 |
min(1.0, progress) |
防止进度条溢出,保持 UI 稳定 |
DateTime.now().millisecondsSinceEpoch |
生成唯一 ID,简单可靠 |
copyWith 模式 |
安全更新植物状态,避免直接修改 |
七、未来扩展方向
当前版本已具备完整核心体验,可进一步拓展:
- 本地持久化 :使用
hive保存植物数据,重启不丢失; - 通知提醒 :集成
flutter_local_notifications,缺水时推送; - 照片记录:允许用户上传植物照片,形成成长日记;
- 品种百科:点击植物查看详细养护指南;
- 成就系统:如"连续浇水30天"、"养活5种植物"等徽章;
- AR 预览 :用 AR 将虚拟植物"放置"在真实桌面(
arkit_flutter_plugin)。
八、结语:在数字世界,种下一棵真实的树
这个植物养护 App 的真正价值,不在于它多么"智能",而在于它唤醒了我们对生命的关注。
当你看到"小绿"的进度条变红,点击"立即浇水",看着它图标旋转、状态变为"茁壮成长"------那一刻,你不是在操作一个 App,而是在履行一份对生命的承诺。
完整代码
bash
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(const PlantCareApp());
}
class PlantCareApp extends StatelessWidget {
const PlantCareApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: '🌿 植物养护',
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.green,
scaffoldBackgroundColor: const Color(0xFFFCFDFC),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
foregroundColor: const Color(0xFF2E7D32),
elevation: 0,
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
home: const PlantCareScreen(),
);
}
}
// 植物数据模型
class Plant {
final String id;
final String name;
final String type; // 多肉/观叶/开花
final int waterInterval; // 浇水间隔(天)
final DateTime lastWatered;
final DateTime plantedDate;
final double growthStage; // 0.0-1.0 生长阶段
Plant({
required this.id,
required this.name,
required this.type,
required this.waterInterval,
required this.lastWatered,
required this.plantedDate,
this.growthStage = 0.0,
});
// 计算是否需要浇水
bool get needsWater {
final daysSinceWater = DateTime.now().difference(lastWatered).inDays;
return daysSinceWater >= waterInterval;
}
// 计算健康状态
String get healthStatus {
if (needsWater) return '需浇水';
final daysSinceWater = DateTime.now().difference(lastWatered).inDays;
if (daysSinceWater <= waterInterval ~/ 2) return '茁壮成长';
return '状态良好';
}
// 获取植物图标(根据类型和生长阶段)
IconData get plantIcon {
if (growthStage < 0.3) return Icons.spa;
if (type == '开花') return Icons.local_florist;
if (type == '多肉') return Icons.local_florist;
return Icons.eco;
}
// 创建新实例(用于状态更新)
Plant copyWith({
DateTime? lastWatered,
double? growthStage,
}) {
return Plant(
id: id,
name: name,
type: type,
waterInterval: waterInterval,
lastWatered: lastWatered ?? this.lastWatered,
plantedDate: plantedDate,
growthStage: growthStage ?? this.growthStage,
);
}
}
class PlantCareScreen extends StatefulWidget {
const PlantCareScreen({super.key});
@override
State<PlantCareScreen> createState() => _PlantCareScreenState();
}
class _PlantCareScreenState extends State<PlantCareScreen> {
// 初始植物数据
List<Plant> _plants = [
Plant(
id: '1',
name: '小绿',
type: '观叶',
waterInterval: 3,
lastWatered: DateTime.now().subtract(const Duration(days: 4)),
plantedDate: DateTime.now().subtract(const Duration(days: 30)),
growthStage: 0.7,
),
Plant(
id: '2',
name: '仙人球',
type: '多肉',
waterInterval: 14,
lastWatered: DateTime.now().subtract(const Duration(days: 10)),
plantedDate: DateTime.now().subtract(const Duration(days: 60)),
growthStage: 0.9,
),
Plant(
id: '3',
name: '茉莉花',
type: '开花',
waterInterval: 2,
lastWatered: DateTime.now().subtract(const Duration(days: 1)),
plantedDate: DateTime.now().subtract(const Duration(days: 15)),
growthStage: 0.4,
),
];
final Random _random = Random();
// 浇水操作
void _waterPlant(String plantId) {
setState(() {
_plants = _plants.map((plant) {
if (plant.id == plantId) {
// 更新浇水时间
final newPlant = plant.copyWith(lastWatered: DateTime.now());
// 随机促进生长(50%概率)
if (_random.nextDouble() > 0.5 && newPlant.growthStage < 1.0) {
final growthIncrement = 0.1 + _random.nextDouble() * 0.15;
return newPlant.copyWith(
growthStage: min(1.0, newPlant.growthStage + growthIncrement),
);
}
return newPlant;
}
return plant;
}).toList();
});
// 显示反馈
final plant = _plants.firstWhere((p) => p.id == plantId);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('${plant.name} 已浇水!${plant.growthStage >= 1.0 ? "已成熟!" : ""}'),
duration: const Duration(seconds: 2),
),
);
}
// 添加新植物
void _addNewPlant() {
final plantNames = ['薄荷', '绿萝', '芦荟', '吊兰', '虎皮兰', '长寿花', '蟹爪兰'];
final types = ['观叶', '多肉', '开花'];
final intervals = [2, 3, 5, 7, 10, 14];
final name = plantNames[_random.nextInt(plantNames.length)];
final type = types[_random.nextInt(types.length)];
final interval = intervals[_random.nextInt(intervals.length)];
final newPlant = Plant(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: name,
type: type,
waterInterval: interval,
lastWatered: DateTime.now(),
plantedDate: DateTime.now(),
);
setState(() {
_plants.add(newPlant);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('欢迎新成员:$name')),
);
}
// 删除植物
void _deletePlant(String plantId) {
final plantToDelete = _plants.firstWhere((p) => p.id == plantId);
setState(() {
_plants.removeWhere((p) => p.id == plantId);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${plantToDelete.name} 已移除'),
action: SnackBarAction(
label: '撤销',
onPressed: () {
setState(() {
_plants.insert(0, plantToDelete);
});
},
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'我的植物',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.info_outline, size: 24),
onPressed: _showCareTips,
),
],
),
body: _plants.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.only(top: 16, bottom: 90),
itemCount: _plants.length,
itemBuilder: (context, index) {
return _buildPlantCard(_plants[index]);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addNewPlant,
child: const Icon(Icons.add),
tooltip: '添加新植物',
),
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.local_florist,
size: 64,
color: Colors.green,
),
),
const SizedBox(height: 24),
const Text(
'还没有植物伙伴',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'点击下方按钮添加你的第一株植物',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey, height: 1.5),
),
],
),
),
);
}
Widget _buildPlantCard(Plant plant) {
final isNeedingWater = plant.needsWater;
final healthColor = isNeedingWater ? Colors.red : Colors.green;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// 植物图标(带动画)
Container(
width: 70,
height: 70,
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return RotationTransition(
turns: Tween<double>(begin: 0, end: 1).animate(animation),
child: child,
);
},
child: Icon(
plant.plantIcon,
key: ValueKey(plant.growthStage),
size: 40,
color: Colors.green.shade700,
),
),
),
const SizedBox(width: 16),
// 植物信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 名称和类型
Row(
children: [
Expanded(
child: Text(
plant.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
onTap: () => _deletePlant(plant.id),
child: const Icon(
Icons.delete_outline,
size: 20,
color: Colors.grey,
),
),
],
),
const SizedBox(height: 4),
Text(
'${plant.type} · 种植${DateTime.now().difference(plant.plantedDate).inDays}天',
style: const TextStyle(color: Colors.grey, fontSize: 14),
),
const SizedBox(height: 12),
// 健康状态
Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: healthColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
plant.healthStatus,
style: TextStyle(
color: healthColor, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 8),
// 浇水进度条
LinearProgressIndicator(
value: min(
1.0,
DateTime.now().difference(plant.lastWatered).inDays /
plant.waterInterval,
),
backgroundColor: Colors.grey.shade200,
color: isNeedingWater ? Colors.red : Colors.green,
minHeight: 6,
borderRadius: BorderRadius.circular(3),
),
],
),
),
const SizedBox(width: 16),
// 浇水按钮
ElevatedButton(
onPressed: () => _waterPlant(plant.id),
style: ElevatedButton.styleFrom(
backgroundColor: isNeedingWater ? Colors.red : Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
padding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
),
child: Text(
isNeedingWater ? '立即浇水' : '浇水',
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold),
),
),
],
),
),
);
}
void _showCareTips() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('🌱 养护小贴士'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('• 观叶植物:保持土壤微湿,避免阳光直射'),
SizedBox(height: 8),
Text('• 多肉植物:宁干勿湿,充足光照'),
SizedBox(height: 8),
Text('• 开花植物:花期需充足水分和磷钾肥'),
SizedBox(height: 16),
Divider(),
SizedBox(height: 8),
Text(
'浇水提示:当进度条变红时,请及时浇水!',
style: TextStyle(color: Colors.red),
),
],
),
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('知道了'),
),
],
),
);
}
}