Flutter for OpenHarmony 植物养护 App:用数字花园培育你的绿色生活

Flutter for OpenHarmony 植物养护 App:用数字花园培育你的绿色生活

在快节奏的都市生活中,一盆绿植不仅是窗台的点缀,更是心灵的慰藉。然而,"忘记浇水""过度溺爱" 常常让我们的植物伙伴悄然枯萎。

🌐 加入社区 欢迎加入 开源鸿蒙跨平台开发者社区 ,获取最新资源与技术支持: 👉 开源鸿蒙跨平台开发者社区


完整效果

一、核心理念:让养护变得智能而有温度

该 App 围绕三个关键体验构建:

  1. 智能提醒:基于浇水间隔自动判断是否"口渴";
  2. 可视化成长:植物图标随生长阶段变化,浇水可能触发"成长动画";
  3. 情感反馈:健康状态文字("茁壮成长"、"需浇水")+ 进度条颜色变化,赋予植物"情绪"。

🌿 技术不应冰冷,而应成为连接人与自然的桥梁


二、数据模型:会"思考"的 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 模式 安全更新植物状态,避免直接修改

七、未来扩展方向

当前版本已具备完整核心体验,可进一步拓展:

  1. 本地持久化 :使用 hive 保存植物数据,重启不丢失;
  2. 通知提醒 :集成 flutter_local_notifications,缺水时推送;
  3. 照片记录:允许用户上传植物照片,形成成长日记;
  4. 品种百科:点击植物查看详细养护指南;
  5. 成就系统:如"连续浇水30天"、"养活5种植物"等徽章;
  6. 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('知道了'),
          ),
        ],
      ),
    );
  }
}
相关推荐
mCell6 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell7 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭7 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清7 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木7 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076607 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
HEADKON7 小时前
索托拉西布Sotorasib治疗KRAS G12C突变肺癌的标准口服方案与药物相互作用
生活·健康医疗
听海边涛声7 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易7 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得08 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化