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('知道了'),
          ),
        ],
      ),
    );
  }
}
相关推荐
Pedantic17 分钟前
SwiftUI 手势笔记
前端·后端
橙子家1 小时前
浏览器缓存之【结构化数据库与缓存】: IndexedDB、Cache storage 和 Storage buckets
前端
user20585561518131 小时前
X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题
前端
李明卫杭州1 小时前
CSS aspect-ratio 属性完全指南
前端
怕浪猫1 小时前
哪些软件对 Chrome DevTools Protocol 频繁使用
人工智能·架构·前端框架
Pedantic3 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘3 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆3 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师4 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端