欢迎光临
我们一直在努力

【maaath】Flutter for OpenHarmony 实战:打造跨平台喝水提醒应用

Flutter for OpenHarmony 实战:打造跨平台喝水提醒应用

社区引导

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

作者:maaath


在移动应用开发领域,如何高效地实现跨平台能力一直是开发者关注的核心议题。Flutter 作为 Google 推出的跨平台 UI 框架,凭借其高性能和一致的 UI 渲染能力,已经在 iOS、Android 等平台取得了广泛应用。而随着 OpenHarmony 生态的蓬勃发展,Flutter for OpenHarmony 成为了连接传统移动开发与新兴国产操作系统的桥梁。本文将通过一个完整的喝水提醒应用实例,展示如何使用 Flutter 实现跨平台开发,并将其部署到鸿蒙设备上运行。

项目背景与需求分析

现代人由于工作繁忙,常常忘记及时补充水分。长期缺水可能导致注意力下降、免疫力减弱等健康问题。一个实用的喝水提醒应用应当具备以下核心功能:

  • 每日饮水记录 – 记录每次喝水的量和时间
  • 饮水目标设定 – 用户可自定义每日饮水目标
  • 喝水提醒通知 – 定时推送提醒通知
  • 饮水习惯统计 – 以图表形式展示饮水数据
  • 快捷添加 – 一键添加预设饮水量
  • 成就激励系统 – 通过成就鼓励用户养成好习惯
  • 本文将详细介绍如何使用 Flutter 实现这些功能,并确保代码能够在鸿蒙设备上正常运行。

    项目结构设计

    良好的项目结构是保证代码可维护性的基础。我们的喝水提醒应用采用分层架构:

    lib/
    ├── main.dart # 应用入口
    ├── models/ # 数据模型层
    │ └── water_model.dart # 喝水相关数据模型
    ├── providers/ # 状态管理层
    │ └── water_provider.dart # 喝水数据状态管理
    ├── pages/ # 页面层
    │ ├── home_page.dart # 首页
    │ ├── stats_page.dart # 统计页面
    │ └── settings_page.dart # 设置页面
    ├── widgets/ # 组件层
    │ ├── water_progress.dart # 进度环形组件
    │ └── quick_add_panel.dart # 快捷添加面板
    └── utils/ # 工具层
    └── notification_helper.dart # 通知工具类

    这种分层结构使得各模块职责清晰,便于后续维护和功能扩展。

    核心数据模型实现

    首先,我们需要定义应用的数据模型。在 Flutter 中,我们可以使用简单的类来定义数据结构:

    // lib/models/water_model.dart

    class WaterRecord {
    String date;
    int totalAmount;
    bool goalAchieved;
    List<WaterEntry> entries;

    WaterRecord({
    required this.date,
    this.totalAmount = 0,
    this.goalAchieved = false,
    List<WaterEntry>? entries,
    }) : entries = entries ?? [];
    }

    class WaterEntry {
    String id;
    String time;
    int amount;
    String period;

    WaterEntry({
    required this.id,
    required this.time,
    required this.amount,
    required this.period,
    });
    }

    class WaterGoal {
    int dailyGoal;
    int cupSize;
    String unit;

    WaterGoal({
    this.dailyGoal = 2000,
    this.cupSize = 250,
    this.unit = 'ml',
    });
    }

    class WaterAchievement {
    String id;
    String name;
    String description;
    String icon;
    bool isUnlocked;
    String unlockCondition;
    int progress;
    int maxProgress;

    WaterAchievement({
    required this.id,
    required this.name,
    required this.description,
    required this.icon,
    this.isUnlocked = false,
    required this.unlockCondition,
    this.progress = 0,
    required this.maxProgress,
    });
    }

    这些模型类定义了应用的核心数据结构,包括饮水记录、饮水条目、饮水目标和成就系统。通过使用 Dart 的类封装,我们确保了数据的类型安全和封装性。

    状态管理实现

    Flutter 提供了多种状态管理方案,这里我们使用 Provider 模式来实现应用状态的集中管理:

    // lib/providers/water_provider.dart

    import 'package:flutter/foundation.dart';
    import '../models/water_model.dart';

    class WaterProvider extends ChangeNotifier {
    List<WaterRecord> _records = [];
    WaterGoal _goal = WaterGoal();
    List<WaterAchievement> _achievements = [];
    List<WaterEntry> _quickAddPresets = [];

    List<WaterRecord> get records => _records;
    WaterGoal get goal => _goal;
    WaterRecord get todayRecord => _getTodayRecord();
    List<WaterAchievement> get achievements => _achievements;
    List<WaterEntry> get quickAddPresets => _quickAddPresets;

    WaterProvider() {
    _initializeData();
    }

    void _initializeData() {
    _quickAddPresets = [
    WaterEntry(id: '1', time: '', amount: 150, period: 'preset'),
    WaterEntry(id: '2', time: '', amount: 250, period: 'preset'),
    WaterEntry(id: '3', time: '', amount: 350, period: 'preset'),
    WaterEntry(id: '4', time: '', amount: 500, period: 'preset'),
    WaterEntry(id: '5', time: '', amount: 750, period: 'preset'),
    WaterEntry(id: '6', time: '', amount: 1000, period: 'preset'),
    ];

    _initMockData();
    _initAchievements();
    }

    void _initMockData() {
    final now = DateTime.now();
    for (int i = 6; i >= 0; i) {
    final date = now.subtract(Duration(days: i));
    final dateStr = _formatDate(date);
    final randomAmount = 800 + (i * 100) + (date.day % 5) * 80;

    _records.add(WaterRecord(
    date: dateStr,
    totalAmount: randomAmount,
    goalAchieved: randomAmount >= _goal.dailyGoal,
    ));
    }
    }

    void _initAchievements() {
    _achievements = [
    WaterAchievement(
    id: 'first_drink',
    name: '初次饮水',
    description: '完成第一次饮水记录',
    icon: '💧',
    unlockCondition: '记录1次饮水',
    maxProgress: 1,
    ),
    WaterAchievement(
    id: 'cup_8',
    name: '八杯水达人',
    description: '单日饮水达到8杯',
    icon: '🥤',
    unlockCondition: '单日8杯水',
    maxProgress: 8,
    ),
    WaterAchievement(
    id: 'goal_7',
    name: '七日王者',
    description: '连续7天达成饮水目标',
    icon: '👑',
    unlockCondition: '连续7天达标',
    maxProgress: 7,
    ),
    ];
    }

    WaterRecord _getTodayRecord() {
    final today = _formatDate(DateTime.now());
    for (final record in _records) {
    if (record.date == today) {
    return record;
    }
    }
    return WaterRecord(date: today);
    }

    void addWaterEntry(int amount) {
    final now = DateTime.now();
    final timeStr = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';

    String period = 'morning';
    if (now.hour >= 12 && now.hour < 18) {
    period = 'afternoon';
    } else if (now.hour >= 18) {
    period = 'evening';
    }

    final entry = WaterEntry(
    id: '${DateTime.now().millisecondsSinceEpoch}',
    time: timeStr,
    amount: amount,
    period: period,
    );

    final today = _getTodayRecord();
    today.entries.add(entry);
    today.totalAmount += amount;
    today.goalAchieved = today.totalAmount >= _goal.dailyGoal;

    _updateAchievements();
    notifyListeners();
    }

    void _updateAchievements() {
    final today = _getTodayRecord();
    final totalEntries = _records.fold<int>(
    0, (sum, record) => sum + record.entries.length
    );

    for (final achievement in _achievements) {
    switch (achievement.id) {
    case 'first_drink':
    achievement.progress = totalEntries > 0 ? 1 : 0;
    achievement.isUnlocked = totalEntries > 0;
    break;
    case 'cup_8':
    final cupCount = today.totalAmount ~/ _goal.cupSize;
    achievement.progress = cupCount;
    achievement.isUnlocked = cupCount >= 8;
    break;
    case 'goal_7':
    final consecutiveDays = _getConsecutiveGoalDays();
    achievement.progress = consecutiveDays;
    achievement.isUnlocked = consecutiveDays >= 7;
    break;
    }
    }
    }

    int _getConsecutiveGoalDays() {
    int consecutive = 0;
    for (int i = 0; i < _records.length; i++) {
    if (_records[_records.length 1 i].goalAchieved) {
    consecutive++;
    } else {
    break;
    }
    }
    return consecutive;
    }

    String _formatDate(DateTime date) {
    final month = (date.month).toString().padLeft(2, '0');
    final day = (date.day).toString().padLeft(2, '0');
    return '$month$day';
    }

    double getProgress() {
    if (_goal.dailyGoal <= 0) return 0;
    return (todayRecord.totalAmount / _goal.dailyGoal).clamp(0.0, 1.0);
    }
    }

    状态管理是 Flutter 应用的核心,它负责协调数据和 UI 之间的同步。通过使用 ChangeNotifier,我们的 WaterProvider 能够在数据变化时通知所有依赖它的组件自动重建界面。

    页面实现

    首页设计

    首页是用户每日使用最频繁的界面,需要直观地展示今日饮水进度和快捷操作入口:

    // lib/pages/home_page.dart

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import '../providers/water_provider.dart';

    class HomePage extends StatelessWidget {
    const HomePage({super.key});


    Widget build(BuildContext context) {
    return ChangeNotifierProvider(
    create: (_) => WaterProvider(),
    child: Scaffold(
    backgroundColor: const Color(0xFFF5F6FA),
    body: SafeArea(
    child: Consumer<WaterProvider>(
    builder: (context, provider, _) {
    return SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    _buildHeader(context),
    const SizedBox(height: 24),
    _buildProgressCard(context, provider),
    const SizedBox(height: 16),
    _buildQuickAddPanel(context, provider),
    const SizedBox(height: 16),
    _buildTodayStats(provider),
    const SizedBox(height: 16),
    _buildAchievementsSection(provider),
    ],
    ),
    );
    },
    ),
    ),
    ),
    );
    }

    Widget _buildHeader(BuildContext context) {
    return Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    const Text(
    '💧 喝水提醒',
    style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Color(0xFF1A1A1A),
    ),
    ),
    Text(
    _getGreeting(),
    style: const TextStyle(
    fontSize: 14,
    color: Color(0xFF999999),
    ),
    ),
    ],
    ),
    Row(
    children: [
    IconButton(
    icon: const Icon(Icons.bar_chart, color: Color(0xFF2196F3)),
    onPressed: () => Navigator.pushNamed(context, '/stats'),
    ),
    IconButton(
    icon: const Icon(Icons.settings, color: Color(0xFF666666)),
    onPressed: () => Navigator.pushNamed(context, '/settings'),
    ),
    ],
    ),
    ],
    );
    }

    Widget _buildProgressCard(BuildContext context, WaterProvider provider) {
    final progress = provider.getProgress();
    final percentage = (progress * 100).toInt();
    final today = provider.todayRecord;

    return Container(
    padding: const EdgeInsets.all(24),
    decoration: BoxDecoration(
    gradient: const LinearGradient(
    colors: [Color(0xFF4CAF50), Color(0xFF2196F3)],
    begin: Alignment.topLeft,
    end: Alignment.bottomRight,
    ),
    borderRadius: BorderRadius.circular(20),
    boxShadow: [
    BoxShadow(
    color: const Color(0xFF4CAF50).withOpacity(0.3),
    blurRadius: 12,
    offset: const Offset(0, 6),
    ),
    ],
    ),
    child: Row(
    children: [
    SizedBox(
    width: 120,
    height: 120,
    child: Stack(
    alignment: Alignment.center,
    children: [
    SizedBox(
    width: 120,
    height: 120,
    child: CircularProgressIndicator(
    value: progress,
    strokeWidth: 10,
    backgroundColor: Colors.white.withOpacity(0.3),
    valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
    ),
    ),
    Column(
    mainAxisSize: MainAxisSize.min,
    children: [
    Text(
    '$percentage%',
    style: const TextStyle(
    fontSize: 28,
    fontWeight: FontWeight.bold,
    color: Colors.white,
    ),
    ),
    const Text(
    '完成度',
    style: TextStyle(
    fontSize: 12,
    color: Colors.white70,
    ),
    ),
    ],
    ),
    ],
    ),
    ),
    const SizedBox(width: 24),
    Expanded(
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    const Text(
    '今日饮水',
    style: TextStyle(
    fontSize: 14,
    color: Colors.white70,
    ),
    ),
    const SizedBox(height: 4),
    RichText(
    text: TextSpan(
    children: [
    TextSpan(
    text: '${today.totalAmount}',
    style: const TextStyle(
    fontSize: 32,
    fontWeight: FontWeight.bold,
    color: Colors.white,
    ),
    ),
    TextSpan(
    text: ' / ${provider.goal.dailyGoal} ml',
    style: const TextStyle(
    fontSize: 16,
    color: Colors.white70,
    ),
    ),
    ],
    ),
    ),
    const SizedBox(height: 8),
    Text(
    '已完成 ${today.entries.length} 次饮水',
    style: const TextStyle(
    fontSize: 12,
    color: Colors.white70,
    ),
    ),
    ],
    ),
    ),
    ],
    ),
    );
    }

    Widget _buildQuickAddPanel(BuildContext context, WaterProvider provider) {
    final presets = [
    {'label': '小杯', 'amount': 150, 'icon': '🥛'},
    {'label': '中杯', 'amount': 250, 'icon': '🥤'},
    {'label': '大杯', 'amount': 350, 'icon': '🍵'},
    {'label': '一瓶', 'amount': 500, 'icon': '🍶'},
    {'label': '大瓶', 'amount': 750, 'icon': '🫗'},
    {'label': '一升', 'amount': 1000, 'icon': '🪣'},
    ];

    return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
    BoxShadow(
    color: Colors.black.withOpacity(0.03),
    blurRadius: 8,
    offset: const Offset(0, 2),
    ),
    ],
    ),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    const Text(
    '⚡ 快速添加',
    style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    color: Color(0xFF1A1A1A),
    ),
    ),
    const SizedBox(height: 12),
    Wrap(
    spacing: 8,
    runSpacing: 8,
    children: presets.map((preset) {
    return InkWell(
    onTap: () {
    provider.addWaterEntry(preset['amount'] as int);
    ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
    content: Text('已添加 ${preset['amount']}ml'),
    duration: const Duration(seconds: 1),
    backgroundColor: const Color(0xFF4CAF50),
    ),
    );
    },
    borderRadius: BorderRadius.circular(12),
    child: Container(
    padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
    decoration: BoxDecoration(
    color: const Color(0xFFF0F2F5),
    borderRadius: BorderRadius.circular(12),
    ),
    child: Row(
    mainAxisSize: MainAxisSize.min,
    children: [
    Text(
    preset['icon'] as String,
    style: const TextStyle(fontSize: 16),
    ),
    const SizedBox(width: 4),
    Text(
    '${preset['amount']}ml',
    style: const TextStyle(
    fontSize: 13,
    fontWeight: FontWeight.w500,
    color: Color(0xFF333333),
    ),
    ),
    ],
    ),
    ),
    );
    }).toList(),
    ),
    ],
    ),
    );
    }

    Widget _buildTodayStats(WaterProvider provider) {
    final today = provider.todayRecord;
    final periodStats = _calculatePeriodStats(today);

    return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
    BoxShadow(
    color: Colors.black.withOpacity(0.03),
    blurRadius: 8,
    offset: const Offset(0, 2),
    ),
    ],
    ),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    const Text(
    '📊 今日时段统计',
    style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    color: Color(0xFF1A1A1A),
    ),
    ),
    const SizedBox(height: 16),
    Row(
    children: periodStats.map((stat) {
    return Expanded(
    child: Container(
    margin: const EdgeInsets.symmetric(horizontal: 4),
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
    color: _getPeriodColor(stat['period'] as String).withOpacity(0.1),
    borderRadius: BorderRadius.circular(12),
    ),
    child: Column(
    children: [
    Icon(
    _getPeriodIcon(stat['period'] as String),
    color: _getPeriodColor(stat['period'] as String),
    size: 24,
    ),
    const SizedBox(height: 4),
    Text(
    stat['label'] as String,
    style: TextStyle(
    fontSize: 11,
    color: _getPeriodColor(stat['period'] as String),
    ),
    ),
    const SizedBox(height: 4),
    Text(
    '${stat['amount']}ml',
    style: const TextStyle(
    fontSize: 14,
    fontWeight: FontWeight.bold,
    color: Color(0xFF1A1A1A),
    ),
    ),
    Text(
    '${stat['count']}次',
    style: const TextStyle(
    fontSize: 11,
    color: Color(0xFF999999),
    ),
    ),
    ],
    ),
    ),
    );
    }).toList(),
    ),
    ],
    ),
    );
    }

    Widget _buildAchievementsSection(WaterProvider provider) {
    return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
    BoxShadow(
    color: Colors.black.withOpacity(0.03),
    blurRadius: 8,
    offset: const Offset(0, 2),
    ),
    ],
    ),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    const Text(
    '🏆 喝水成就',
    style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    color: Color(0xFF1A1A1A),
    ),
    ),
    Text(
    '${provider.achievements.where((a) => a.isUnlocked).length}/${provider.achievements.length}',
    style: const TextStyle(
    fontSize: 12,
    color: Color(0xFF999999),
    ),
    ),
    ],
    ),
    const SizedBox(height: 12),
    ...provider.achievements.map((achievement) {
    return Container(
    margin: const EdgeInsets.only(bottom: 8),
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
    color: achievement.isUnlocked
    ? const Color(0xFFFFF8E1)
    : const Color(0xFFF5F5F5),
    borderRadius: BorderRadius.circular(12),
    ),
    child: Row(
    children: [
    Text(
    achievement.icon,
    style: TextStyle(
    fontSize: 24,
    color: achievement.isUnlocked ? null : Colors.grey,
    ),
    ),
    const SizedBox(width: 12),
    Expanded(
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    Text(
    achievement.name,
    style: TextStyle(
    fontSize: 14,
    fontWeight: FontWeight.w500,
    color: achievement.isUnlocked
    ? const Color(0xFF1A1A1A)
    : const Color(0xFF999999),
    ),
    ),
    const SizedBox(height: 2),
    Text(
    achievement.description,
    style: TextStyle(
    fontSize: 11,
    color: achievement.isUnlocked
    ? const Color(0xFF666666)
    : const Color(0xFFBBBBBB),
    ),
    ),
    ],
    ),
    ),
    if (achievement.isUnlocked)
    const Icon(
    Icons.check_circle,
    color: Color(0xFF4CAF50),
    size: 20,
    )
    else
    Text(
    '${achievement.progress}/${achievement.maxProgress}',
    style: const TextStyle(
    fontSize: 12,
    color: Color(0xFF999999),
    ),
    ),
    ],
    ),
    );
    }),
    ],
    ),
    );
    }

    List<Map<String, dynamic>> _calculatePeriodStats(WaterRecord today) {
    int morning = 0, morningCount = 0;
    int afternoon = 0, afternoonCount = 0;
    int evening = 0, eveningCount = 0;

    for (final entry in today.entries) {
    switch (entry.period) {
    case 'morning':
    morning += entry.amount;
    morningCount++;
    break;
    case 'afternoon':
    afternoon += entry.amount;
    afternoonCount++;
    break;
    case 'evening':
    evening += entry.amount;
    eveningCount++;
    break;
    }
    }

    return [
    {'period': 'morning', 'label': '上午', 'amount': morning, 'count': morningCount},
    {'period': 'afternoon', 'label': '下午', 'amount': afternoon, 'count': afternoonCount},
    {'period': 'evening', 'label': '晚上', 'amount': evening, 'count': eveningCount},
    ];
    }

    Color _getPeriodColor(String period) {
    switch (period) {
    case 'morning':
    return const Color(0xFFFF9800);
    case 'afternoon':
    return const Color(0xFF2196F3);
    case 'evening':
    return const Color(0xFF9C27B0);
    default:
    return Colors.grey;
    }
    }

    IconData _getPeriodIcon(String period) {
    switch (period) {
    case 'morning':
    return Icons.wb_sunny;
    case 'afternoon':
    return Icons.wb_cloudy;
    case 'evening':
    return Icons.nights_stay;
    default:
    return Icons.water_drop;
    }
    }

    String _getGreeting() {
    final hour = DateTime.now().hour;
    if (hour < 6) {
    return '夜深了,记得喝水哦~';
    } else if (hour < 9) {
    return '早上好,开始新的一天~';
    } else if (hour < 12) {
    return '上午好,及时补水精神好~';
    } else if (hour < 14) {
    return '中午好,午餐后别忘喝水~';
    } else if (hour < 18) {
    return '下午好,保持水分摄入~';
    } else if (hour < 22) {
    return '晚上好,睡前记得喝水~';
    } else {
    return '夜深了,注意休息~';
    }
    }
    }

    首页设计充分考虑了用户体验,通过环形进度条直观展示今日饮水进度,快捷添加面板让用户能够一键记录饮水量,而成就系统则通过游戏化的方式激励用户养成良好的饮水习惯。

    统计页面实现

    统计页面以图表形式展示用户的饮水习惯,帮助用户更好地了解自己的饮水模式:

    // lib/pages/stats_page.dart

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import '../providers/water_provider.dart';

    class StatsPage extends StatelessWidget {
    const StatsPage({super.key});


    Widget build(BuildContext context) {
    return ChangeNotifierProvider(
    create: (_) => WaterProvider(),
    child: Scaffold(
    backgroundColor: const Color(0xFFF5F6FA),
    appBar: AppBar(
    backgroundColor: Colors.white,
    elevation: 0,
    leading: IconButton(
    icon: const Icon(Icons.arrow_back, color: Color(0xFF1A1A1A)),
    onPressed: () => Navigator.pop(context),
    ),
    title: const Text(
    '饮水统计',
    style: TextStyle(
    color: Color(0xFF1A1A1A),
    fontWeight: FontWeight.bold,
    ),
    ),
    ),
    body: Consumer<WaterProvider>(
    builder: (context, provider, _) {
    return SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    _buildWeeklyChart(provider),
    const SizedBox(height: 16),
    _buildWeeklySummary(provider),
    const SizedBox(height: 16),
    _buildHistoryList(provider),
    ],
    ),
    );
    },
    ),
    ),
    );
    }

    Widget _buildWeeklyChart(WaterProvider provider) {
    final records = provider.records;
    final maxAmount = records.fold<int>(
    0, (max, record) => record.totalAmount > max ? record.totalAmount : max
    );

    return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
    BoxShadow(
    color: Colors.black.withOpacity(0.03),
    blurRadius: 8,
    offset: const Offset(0, 2),
    ),
    ],
    ),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    const Text(
    '📈 近7天饮水趋势',
    style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    color: Color(0xFF1A1A1A),
    ),
    ),
    const SizedBox(height: 24),
    SizedBox(
    height: 180,
    child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceAround,
    crossAxisAlignment: CrossAxisAlignment.end,
    children: records.map((record) {
    final height = maxAmount > 0
    ? (record.totalAmount / maxAmount * 140).clamp(20.0, 140.0)
    : 20.0;
    final isToday = record.date == _getTodayDateStr();

    return Column(
    mainAxisAlignment: MainAxisAlignment.end,
    children: [
    Text(
    '${record.totalAmount}',
    style: TextStyle(
    fontSize: 10,
    color: isToday ? const Color(0xFF4CAF50) : const Color(0xFF999999),
    ),
    ),
    const SizedBox(height: 4),
    AnimatedContainer(
    duration: const Duration(milliseconds: 300),
    width: 32,
    height: height,
    decoration: BoxDecoration(
    gradient: LinearGradient(
    colors: isToday
    ? [const Color(0xFF4CAF50), const Color(0xFF8BC34A)]
    : [const Color(0xFF2196F3), const Color(0xFF64B5F6)],
    begin: Alignment.bottomCenter,
    end: Alignment.topCenter,
    ),
    borderRadius: BorderRadius.circular(8),
    ),
    ),
    const SizedBox(height: 8),
    Text(
    record.date.split('-').last,
    style: TextStyle(
    fontSize: 11,
    color: isToday ? const Color(0xFF4CAF50) : const Color(0xFF666666),
    fontWeight: isToday ? FontWeight.bold : FontWeight.normal,
    ),
    ),
    ],
    );
    }).toList(),
    ),
    ),
    ],
    ),
    );
    }

    Widget _buildWeeklySummary(WaterProvider provider) {
    final records = provider.records;
    final totalAmount = records.fold<int>(0, (sum, r) => sum + r.totalAmount);
    final avgAmount = records.isNotEmpty ? totalAmount ~/ records.length : 0;
    final goalDays = records.where((r) => r.goalAchieved).length;
    final goalRate = records.isNotEmpty ? (goalDays / records.length * 100).toInt() : 0;

    return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
    BoxShadow(
    color: Colors.black.withOpacity(0.03),
    blurRadius: 8,
    offset: const Offset(0, 2),
    ),
    ],
    ),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    const Text(
    '📋 周统计摘要',
    style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    color: Color(0xFF1A1A1A),
    ),
    ),
    const SizedBox(height: 16),
    Row(
    children: [
    _buildSummaryItem('总饮水量', '${totalAmount}ml', const Color(0xFF2196F3)),
    _buildSummaryItem('日均饮水量', '${avgAmount}ml', const Color(0xFF4CAF50)),
    _buildSummaryItem('达标天数', '$goalDays/${records.length}天', const Color(0xFFFF9800)),
    _buildSummaryItem('达标率', '$goalRate%', const Color(0xFF9C27B0)),
    ],
    ),
    ],
    ),
    );
    }

    Widget _buildSummaryItem(String label, String value, Color color) {
    return Expanded(
    child: Column(
    children: [
    Text(
    value,
    style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.bold,
    color: color,
    ),
    ),
    const SizedBox(height: 4),
    Text(
    label,
    style: const TextStyle(
    fontSize: 11,
    color: Color(0xFF999999),
    ),
    textAlign: TextAlign.center,
    ),
    ],
    ),
    );
    }

    Widget _buildHistoryList(WaterProvider provider) {
    return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
    BoxShadow(
    color: Colors.black.withOpacity(0.03),
    blurRadius: 8,
    offset: const Offset(0, 2),
    ),
    ],
    ),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    const Text(
    '📜 饮水记录',
    style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    color: Color(0xFF1A1A1A),
    ),
    ),
    const SizedBox(height: 12),
    ...provider.records.reversed.map((record) {
    final isToday = record.date == _getTodayDateStr();
    return Container(
    margin: const EdgeInsets.only(bottom: 8),
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
    color: isToday ? const Color(0xFFF0FFF0) : const Color(0xFFF8F8F8),
    borderRadius: BorderRadius.circular(12),
    border: isToday ? Border.all(color: const Color(0xFF4CAF50), width: 1) : null,
    ),
    child: Row(
    children: [
    Container(
    width: 40,
    height: 40,
    decoration: BoxDecoration(
    color: isToday ? const Color(0xFF4CAF50) : const Color(0xFF2196F3),
    borderRadius: BorderRadius.circular(10),
    ),
    child: Center(
    child: Text(
    record.date.split('-').last,
    style: const TextStyle(
    color: Colors.white,
    fontWeight: FontWeight.bold,
    fontSize: 12,
    ),
    ),
    ),
    ),
    const SizedBox(width: 12),
    Expanded(
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    Text(
    '${record.totalAmount}ml',
    style: const TextStyle(
    fontSize: 15,
    fontWeight: FontWeight.w600,
    color: Color(0xFF1A1A1A),
    ),
    ),
    Text(
    '${record.entries.length}次饮水',
    style: const TextStyle(
    fontSize: 12,
    color: Color(0xFF999999),
    ),
    ),
    ],
    ),
    ),
    if (record.goalAchieved)
    Container(
    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
    decoration: BoxDecoration(
    color: const Color(0xFF4CAF50),
    borderRadius: BorderRadius.circular(12),
    ),
    child: const Text(
    '✓ 已达标',
    style: TextStyle(
    fontSize: 11,
    color: Colors.white,
    fontWeight: FontWeight.w500,
    ),
    ),
    ),
    ],
    ),
    );
    }),
    ],
    ),
    );
    }

    String _getTodayDateStr() {
    final now = DateTime.now();
    return '${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
    }
    }

    设置页面实现

    设置页面允许用户自定义饮水目标和其他偏好设置:

    // lib/pages/settings_page.dart

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import '../providers/water_provider.dart';

    class SettingsPage extends StatefulWidget {
    const SettingsPage({super.key});


    State<SettingsPage> createState() => _SettingsPageState();
    }

    class _SettingsPageState extends State<SettingsPage> {
    double _dailyGoal = 2000;
    double _cupSize = 250;
    bool _notificationsEnabled = true;
    int _reminderInterval = 90;


    Widget build(BuildContext context) {
    return ChangeNotifierProvider(
    create: (_) => WaterProvider(),
    child: Scaffold(
    backgroundColor: const Color(0xFFF5F6FA),
    appBar: AppBar(
    backgroundColor: Colors.white,
    elevation: 0,
    leading: IconButton(
    icon: const Icon(Icons.arrow_back, color: Color(0xFF1A1A1A)),
    onPressed: () => Navigator.pop(context),
    ),
    title: const Text(
    '设置',
    style: TextStyle(
    color: Color(0xFF1A1A1A),
    fontWeight: FontWeight.bold,
    ),
    ),
    ),
    body: SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    _buildGoalSettings(),
    const SizedBox(height: 16),
    _buildReminderSettings(),
    const SizedBox(height: 16),
    _buildAboutSection(),
    ],
    ),
    ),
    ),
    );
    }

    Widget _buildGoalSettings() {
    return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
    BoxShadow(
    color: Colors.black.withOpacity(0.03),
    blurRadius: 8,
    offset: const Offset(0, 2),
    ),
    ],
    ),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    const Text(
    '🎯 饮水目标',
    style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    color: Color(0xFF1A1A1A),
    ),
    ),
    const SizedBox(height: 20),
    _buildSettingItem(
    '每日目标',
    '${_dailyGoal.toInt()}ml',
    Icons.flag,
    const Color(0xFF4CAF50),
    ),
    Slider(
    value: _dailyGoal,
    min: 1000,
    max: 4000,
    divisions: 30,
    activeColor: const Color(0xFF4CAF50),
    onChanged: (value) {
    setState(() => _dailyGoal = value);
    },
    ),
    Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: const [
    Text('1000ml', style: TextStyle(fontSize: 11, color: Color(0xFF999999))),
    Text('4000ml', style: TextStyle(fontSize: 11, color: Color(0xFF999999))),
    ],
    ),
    const SizedBox(height: 16),
    _buildSettingItem(
    '杯容量',
    '${_cupSize.toInt()}ml',
    Icons.local_cafe,
    const Color(0xFF2196F3),
    ),
    Slider(
    value: _cupSize,
    min: 100,
    max: 500,
    divisions: 8,
    activeColor: const Color(0xFF2196F3),
    onChanged: (value) {
    setState(() => _cupSize = value);
    },
    ),
    Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: const [
    Text('100ml', style: TextStyle(fontSize: 11, color: Color(0xFF999999))),
    Text('500ml', style: TextStyle(fontSize: 11, color: Color(0xFF999999))),
    ],
    ),
    ],
    ),
    );
    }

    Widget _buildReminderSettings() {
    return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
    BoxShadow(
    color: Colors.black.withOpacity(0.03),
    blurRadius: 8,
    offset: const Offset(0, 2),
    ),
    ],
    ),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    const Text(
    '🔔 提醒设置',
    style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    color: Color(0xFF1A1A1A),
    ),
    ),
    Switch(
    value: _notificationsEnabled,
    activeColor: const Color(0xFF4CAF50),
    onChanged: (value) {
    setState(() => _notificationsEnabled = value);
    },
    ),
    ],
    ),
    if (_notificationsEnabled) ...[
    const SizedBox(height: 16),
    _buildSettingItem(
    '提醒间隔',
    '每${_reminderInterval}分钟',
    Icons.timer,
    const Color(0xFFFF9800),
    ),
    Slider(
    value: _reminderInterval.toDouble(),
    min: 30,
    max: 240,
    divisions: 14,
    activeColor: const Color(0xFFFF9800),
    onChanged: (value) {
    setState(() => _reminderInterval = value.toInt());
    },
    ),
    Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: const [
    Text('30分钟', style: TextStyle(fontSize: 11, color: Color(0xFF999999))),
    Text('4小时', style: TextStyle(fontSize: 11, color: Color(0xFF999999))),
    ],
    ),
    ],
    ],
    ),
    );
    }

    Widget _buildAboutSection() {
    return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
    BoxShadow(
    color: Colors.black.withOpacity(0.03),
    blurRadius: 8,
    offset: const Offset(0, 2),
    ),
    ],
    ),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    const Text(
    'ℹ️ 关于',
    style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    color: Color(0xFF1A1A1A),
    ),
    ),
    const SizedBox(height: 16),
    _buildInfoRow('应用版本', '1.0.0'),
    _buildInfoRow('开发者', 'maaath'),
    _buildInfoRow('开源协议', 'MIT License'),
    const SizedBox(height: 16),
    const Text(
    '喝水提醒应用 – 使用 Flutter for OpenHarmony 构建',
    style: TextStyle(
    fontSize: 12,
    color: Color(0xFF999999),
    ),
    ),
    ],
    ),
    );
    }

    Widget _buildSettingItem(String title, String value, IconData icon, Color color) {
    return Padding(
    padding: const EdgeInsets.only(bottom: 8),
    child: Row(
    children: [
    Container(
    width: 32,
    height: 32,
    decoration: BoxDecoration(
    color: color.withOpacity(0.1),
    borderRadius: BorderRadius.circular(8),
    ),
    child: Icon(icon, color: color, size: 18),
    ),
    const SizedBox(width: 12),
    Text(
    title,
    style: const TextStyle(
    fontSize: 14,
    color: Color(0xFF333333),
    ),
    ),
    const Spacer(),
    Text(
    value,
    style: TextStyle(
    fontSize: 14,
    fontWeight: FontWeight.w600,
    color: color,
    ),
    ),
    ],
    ),
    );
    }

    Widget _buildInfoRow(String label, String value) {
    return Padding(
    padding: const EdgeInsets.only(bottom: 8),
    child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    Text(
    label,
    style: const TextStyle(
    fontSize: 14,
    color: Color(0xFF666666),
    ),
    ),
    Text(
    value,
    style: const TextStyle(
    fontSize: 14,
    color: Color(0xFF1A1A1A),
    ),
    ),
    ],
    ),
    );
    }
    }

    应用入口文件

    最后,我们需要创建应用的主入口文件,配置路由和整体布局:

    // lib/main.dart

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import 'pages/home_page.dart';
    import 'pages/stats_page.dart';
    import 'pages/settings_page.dart';

    void main() {
    runApp(const WaterReminderApp());
    }

    class WaterReminderApp extends StatelessWidget {
    const WaterReminderApp({super.key});


    Widget build(BuildContext context) {
    return MaterialApp(
    title: '喝水提醒',
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
    primarySwatch: Colors.blue,
    useMaterial3: true,
    fontFamily: 'System',
    ),
    home: const HomePage(),
    routes: {
    '/stats': (context) => const StatsPage(),
    '/settings': (context) => const SettingsPage(),
    },
    );
    }
    }

    鸿蒙设备运行验证

    完成代码编写后,我们需要将应用部署到鸿蒙设备上进行验证。Flutter for OpenHarmony 提供了与标准 Flutter 开发高度一致的开发体验,开发者只需执行以下命令即可完成应用构建和部署:

    # 构建调试版本
    flutter build hap –debug

    # 构建发布版本
    flutter build hap –release

    应用成功运行后,用户可以看到以下主要界面:

    主界面 – 展示今日饮水进度、快捷添加面板、时段统计和成就系统:
    在这里插入图片描述

    应用顶部显示问候语和功能入口,中间是醒目的环形进度条直观展示饮水完成度,快捷添加面板提供了一键记录饮水的便捷方式,底部则展示了时段统计和成就解锁情况。

    统计页面 – 展示近7天饮水趋势图和周统计摘要:
    在这里插入图片描述

    通过柱状图形式展示每日饮水量对比,配合达标天数统计帮助用户了解自己的饮水习惯。

    设置页面 – 自定义每日目标和提醒设置:
    在这里插入图片描述

    滑动条式的设置交互提供了良好的用户体验,支持自定义每日饮水目标和提醒间隔。

    跨平台开发经验总结

    通过这个喝水提醒应用的开发实践,我总结了以下几点 Flutter 跨平台开发的关键经验:

    状态管理策略:对于中小型应用,Provider 模式已经足够满足需求。它简单易用,与 Flutter 框架深度集成,无需引入额外的第三方依赖。

    UI 组件复用:Flutter 的组件化设计使得代码复用变得非常自然。我们可以将常用的 UI 模式封装为独立组件,如进度环形图、统计卡片等,在不同页面中复用。

    平台差异处理:虽然 Flutter 提供了统一的 API,但在实际开发中仍需注意平台差异。例如通知功能,在不同平台上的实现方式可能有所不同,建议使用条件导入来处理平台特定的代码。

    性能优化要点:合理使用 const 构造函数可以减少不必要的重建,使用 RepaintBoundary 隔离频繁变化的区域,以及避免在 build 方法中执行耗时操作。

    代码托管

    本项目的完整代码已托管至 AtomGit 平台,欢迎开发者交流学习:

    仓库地址:https://atomgit.com/maaath/water-reminder-app

    结语

    本文通过一个完整的喝水提醒应用实例,展示了 Flutter for OpenHarmony 跨平台开发的完整流程。从需求分析、架构设计,到代码实现、鸿蒙设备运行验证,我们可以看到 Flutter 框架在跨平台开发中的强大能力。

    随着 OpenHarmony 生态的持续发展,Flutter for OpenHarmony 将成为越来越多开发者选择跨平台方案时的首选。它不仅保留了 Flutter 原有的开发效率优势,还能够充分利用 OpenHarmony 的系统能力,为用户带来原生的使用体验。

    希望本文能够为正在进行或计划开展 Flutter for OpenHarmony 开发的开发者提供一些参考和启发。如果您有任何问题或建议,欢迎在社区中交流讨论。


    赞(0)
    未经允许不得转载:171主机测评 » 【maaath】Flutter for OpenHarmony 实战:打造跨平台喝水提醒应用
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址