Flutter 框架跨平台鸿蒙开发 - 运动健身打卡:打造你的专属健身助手

Flutter运动健身打卡:打造你的专属健身助手

项目简介

运动健身打卡是一款帮助用户养成运动习惯的移动应用,支持多种运动类型记录、数据统计分析、目标管理和成就系统。通过可视化的数据展示和激励机制,让运动变得更有趣、更有动力。
运行效果图




核心功能

  • 多类型运动打卡:支持跑步、骑行、游泳、瑜伽等8种运动类型
  • 数据统计分析:周/月/年维度的运动数据统计和可视化图表
  • 连续打卡激励:显示连续打卡天数,激发坚持动力
  • 目标管理:设置运动目标,追踪完成进度
  • 成就系统:累计数据展示,记录运动里程碑

技术特点

技术 说明
Flutter 3.0+
Material Design 3 现代化UI设计
底部导航 NavigationBar组件
状态管理 StatefulWidget
数据可视化 自定义图表组件

功能架构

运动健身打卡
打卡页面
记录页面
统计页面
个人页面
今日数据
连续打卡
运动类型选择
打卡记录
按日期分组
记录详情
搜索功能
周期选择
整体统计
趋势图表
类型分布
目标进度
用户信息
运动成就
功能设置

核心功能详解

1. 运动打卡页面

打卡页面是应用的核心,提供快速打卡入口和今日数据展示。

页面布局:

  • 今日运动数据卡片(时长、消耗、次数)
  • 连续打卡天数展示
  • 8种运动类型网格
  • 今日打卡记录列表

打卡流程:

  1. 选择运动类型
  2. 输入运动时长和距离
  3. 添加备注(可选)
  4. 确认打卡
  5. 自动计算消耗卡路里

卡路里计算公式:

dart 复制代码
final calories = (duration * 5.5).round();
// 简化计算:每分钟消耗5.5千卡
// 实际应用可根据运动类型、体重等因素精确计算

2. 运动记录页面

记录页面展示历史运动数据,按日期分组显示。

数据分组:

  • 今天
  • 昨天
  • 前天
  • 具体日期(X月X日)

记录信息:

  • 运动类型图标和名称
  • 运动时长和消耗
  • 运动距离(如有)
  • 备注信息
  • 运动时间

日期格式化:

dart 复制代码
String _formatDate(DateTime date) {
  final now = DateTime.now();
  final today = DateTime(now.year, now.month, now.day);
  final recordDate = DateTime(date.year, date.month, date.day);
  final diff = today.difference(recordDate).inDays;

  if (diff == 0) return '今天';
  if (diff == 1) return '昨天';
  if (diff == 2) return '前天';
  return '${date.month}月${date.day}日';
}

3. 数据统计页面

统计页面提供多维度的数据分析和可视化展示。

统计维度:

  • 周统计:最近7天数据
  • 月统计:当月数据
  • 年统计:全年数据

统计内容:

  1. 整体数据:运动天数、总时长、总消耗
  2. 趋势图表:每日运动时长柱状图
  3. 类型分布:各运动类型占比
  4. 目标进度:运动目标完成情况

柱状图实现:

dart 复制代码
Widget _buildWeeklyChart() {
  final weekData = [
    {'day': '一', 'value': 45},
    {'day': '二', 'value': 60},
    // ...
  ];

  final maxValue = weekData.map((d) => d['value'] as int).reduce(max);

  return Row(
    children: weekData.map((data) {
      final value = data['value'] as int;
      final height = maxValue > 0 ? (value / maxValue * 100) : 0.0;
      return Container(
        width: 30,
        height: height.clamp(10, 100),
        decoration: BoxDecoration(
          color: value > 0 ? Colors.orange : Colors.grey[300],
          borderRadius: BorderRadius.circular(4),
        ),
      );
    }).toList(),
  );
}

4. 个人中心页面

个人页面展示用户信息、运动成就和应用设置。

用户信息:

  • 头像和昵称
  • 坚持天数
  • 等级徽章

运动成就:

  • 累计打卡天数
  • 累计消耗卡路里
  • 累计运动时长
  • 最长连续天数

功能菜单:

  • 我的目标
  • 运动提醒
  • 分享成就
  • 应用设置
  • 帮助反馈
  • 关于应用

数据模型设计

运动类型枚举

dart 复制代码
enum ExerciseType {
  running('跑步', Icons.directions_run, Colors.orange),
  cycling('骑行', Icons.directions_bike, Colors.blue),
  swimming('游泳', Icons.pool, Colors.cyan),
  yoga('瑜伽', Icons.self_improvement, Colors.purple),
  gym('健身房', Icons.fitness_center, Colors.red),
  walking('步行', Icons.directions_walk, Colors.green),
  basketball('篮球', Icons.sports_basketball, Colors.deepOrange),
  football('足球', Icons.sports_soccer, Colors.teal);

  final String label;
  final IconData icon;
  final Color color;
  const ExerciseType(this.label, this.icon, this.color);
}

打卡记录模型

dart 复制代码
class CheckInRecord {
  final String id;
  final ExerciseType type;
  final DateTime date;
  final int duration;        // 运动时长(分钟)
  final double distance;     // 运动距离(公里)
  final int calories;        // 消耗卡路里
  final String note;         // 备注

  CheckInRecord({
    required this.id,
    required this.type,
    required this.date,
    required this.duration,
    this.distance = 0,
    required this.calories,
    this.note = '',
  });
}

运动目标模型

dart 复制代码
class FitnessGoal {
  final String id;
  final String title;
  final int targetDays;      // 目标天数
  final int completedDays;   // 已完成天数
  final DateTime startDate;
  final DateTime endDate;

  FitnessGoal({
    required this.id,
    required this.title,
    required this.targetDays,
    required this.completedDays,
    required this.startDate,
    required this.endDate,
  });

  double get progress => completedDays / targetDays;
}

核心代码实现

打卡对话框

dart 复制代码
void _showCheckInDialog(ExerciseType type) {
  final durationController = TextEditingController(text: '30');
  final distanceController = TextEditingController(text: '0');
  final noteController = TextEditingController();

  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Row(
        children: [
          Icon(type.icon, color: type.color),
          const SizedBox(width: 8),
          Text(type.label),
        ],
      ),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: durationController,
            keyboardType: TextInputType.number,
            decoration: const InputDecoration(
              labelText: '运动时长(分钟)',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 12),
          TextField(
            controller: distanceController,
            keyboardType: TextInputType.number,
            decoration: const InputDecoration(
              labelText: '运动距离(公里)',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 12),
          TextField(
            controller: noteController,
            decoration: const InputDecoration(
              labelText: '备注(可选)',
              border: OutlineInputBorder(),
            ),
            maxLines: 2,
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        FilledButton(
          onPressed: () {
            // 保存打卡记录
            final duration = int.tryParse(durationController.text) ?? 0;
            final distance = double.tryParse(distanceController.text) ?? 0;
            final calories = (duration * 5.5).round();

            setState(() {
              _todayRecords.add(CheckInRecord(
                id: DateTime.now().toString(),
                type: type,
                date: DateTime.now(),
                duration: duration,
                distance: distance,
                calories: calories,
                note: noteController.text,
              ));
            });

            Navigator.pop(context);
          },
          child: const Text('确认打卡'),
        ),
      ],
    ),
  );
}

记录分组显示

dart 复制代码
Map<String, List<CheckInRecord>> _groupRecordsByDate() {
  final Map<String, List<CheckInRecord>> grouped = {};
  for (var record in _records) {
    final dateKey = _formatDate(record.date);
    grouped.putIfAbsent(dateKey, () => []).add(record);
  }
  return grouped;
}

Widget _buildDateGroup(String date, List<CheckInRecord> records) {
  final totalDuration = records.fold<int>(0, (sum, r) => sum + r.duration);
  final totalCalories = records.fold<int>(0, (sum, r) => sum + r.calories);

  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Row(
        children: [
          Text(date, style: const TextStyle(
            fontSize: 16, 
            fontWeight: FontWeight.bold
          )),
          const SizedBox(width: 16),
          Text(
            '$totalDuration分钟 · $totalCalories千卡',
            style: TextStyle(fontSize: 14, color: Colors.grey[600]),
          ),
        ],
      ),
      ...records.map((record) => _buildRecordCard(record)),
    ],
  );
}

统计数据计算

dart 复制代码
// 计算周期内的统计数据
Map<String, dynamic> _calculateStats(String period) {
  int days = 0;
  int totalDuration = 0;
  int totalCalories = 0;
  
  final now = DateTime.now();
  DateTime startDate;
  
  switch (period) {
    case '周':
      startDate = now.subtract(const Duration(days: 7));
      break;
    case '月':
      startDate = DateTime(now.year, now.month, 1);
      break;
    case '年':
      startDate = DateTime(now.year, 1, 1);
      break;
    default:
      startDate = now.subtract(const Duration(days: 7));
  }
  
  final periodRecords = _records.where((r) => 
    r.date.isAfter(startDate) && r.date.isBefore(now)
  ).toList();
  
  // 计算运动天数(去重)
  final uniqueDates = periodRecords.map((r) => 
    DateTime(r.date.year, r.date.month, r.date.day)
  ).toSet();
  days = uniqueDates.length;
  
  // 计算总时长和总消耗
  for (var record in periodRecords) {
    totalDuration += record.duration;
    totalCalories += record.calories;
  }
  
  return {
    'days': days,
    'duration': totalDuration,
    'calories': totalCalories,
  };
}

目标进度展示

dart 复制代码
Widget _buildGoalCard(FitnessGoal goal) {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                goal.title, 
                style: const TextStyle(
                  fontSize: 16, 
                  fontWeight: FontWeight.bold
                ),
              ),
              Text(
                '${goal.completedDays}/${goal.targetDays}',
                style: const TextStyle(
                  fontSize: 14, 
                  color: Colors.orange
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          LinearProgressIndicator(
            value: goal.progress,
            backgroundColor: Colors.grey[200],
            valueColor: const AlwaysStoppedAnimation<Color>(
              Colors.orange
            ),
          ),
          const SizedBox(height: 8),
          Text(
            '已完成 ${(goal.progress * 100).toStringAsFixed(0)}%',
            style: TextStyle(fontSize: 12, color: Colors.grey[600]),
          ),
        ],
      ),
    ),
  );
}

界面设计要点

1. 底部导航栏

使用Material 3的NavigationBar组件:

dart 复制代码
NavigationBar(
  selectedIndex: _currentIndex,
  onDestinationSelected: (index) {
    setState(() => _currentIndex = index);
  },
  destinations: const [
    NavigationDestination(icon: Icon(Icons.add_circle), label: '打卡'),
    NavigationDestination(icon: Icon(Icons.list), label: '记录'),
    NavigationDestination(icon: Icon(Icons.bar_chart), label: '统计'),
    NavigationDestination(icon: Icon(Icons.person), label: '我的'),
  ],
)

2. 卡片布局

统一使用Card组件,保持视觉一致性:

dart 复制代码
Card(
  child: Padding(
    padding: const EdgeInsets.all(20),
    child: Column(
      children: [
        // 卡片内容
      ],
    ),
  ),
)

3. 网格布局

运动类型使用4列网格:

dart 复制代码
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 4,
    crossAxisSpacing: 12,
    mainAxisSpacing: 12,
  ),
  itemBuilder: (context, index) {
    return _buildExerciseCard(type);
  },
)

4. 颜色系统

用途 颜色 说明
主色调 Orange 活力、运动感
跑步 Orange 热情
骑行 Blue 自由
游泳 Cyan 清爽
瑜伽 Purple 优雅
健身房 Red 力量
步行 Green 健康

功能优化建议

1. 数据持久化

使用SharedPreferences或SQLite保存数据:

dart 复制代码
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class DataService {
  // 保存打卡记录
  Future<void> saveRecords(List<CheckInRecord> records) async {
    final prefs = await SharedPreferences.getInstance();
    final jsonList = records.map((r) => {
      'id': r.id,
      'type': r.type.name,
      'date': r.date.toIso8601String(),
      'duration': r.duration,
      'distance': r.distance,
      'calories': r.calories,
      'note': r.note,
    }).toList();
    await prefs.setString('records', jsonEncode(jsonList));
  }

  // 读取打卡记录
  Future<List<CheckInRecord>> loadRecords() async {
    final prefs = await SharedPreferences.getInstance();
    final jsonStr = prefs.getString('records');
    if (jsonStr == null) return [];
    
    final jsonList = jsonDecode(jsonStr) as List;
    return jsonList.map((json) => CheckInRecord(
      id: json['id'],
      type: ExerciseType.values.firstWhere(
        (e) => e.name == json['type']
      ),
      date: DateTime.parse(json['date']),
      duration: json['duration'],
      distance: json['distance'],
      calories: json['calories'],
      note: json['note'],
    )).toList();
  }
}

2. 运动提醒功能

使用flutter_local_notifications实现定时提醒:

dart 复制代码
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

class NotificationService {
  final FlutterLocalNotificationsPlugin _plugin = 
    FlutterLocalNotificationsPlugin();

  Future<void> init() async {
    const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
    const iosSettings = DarwinInitializationSettings();
    const settings = InitializationSettings(
      android: androidSettings,
      iOS: iosSettings,
    );
    await _plugin.initialize(settings);
  }

  Future<void> scheduleDailyReminder(int hour, int minute) async {
    await _plugin.zonedSchedule(
      0,
      '运动提醒',
      '该运动啦!坚持就是胜利!',
      _nextInstanceOfTime(hour, minute),
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'daily_reminder',
          '每日提醒',
          channelDescription: '每日运动提醒',
          importance: Importance.high,
        ),
      ),
      androidAllowWhileIdle: true,
      uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: DateTimeComponents.time,
    );
  }
}

3. GPS轨迹记录

集成geolocator实现跑步轨迹记录:

dart 复制代码
import 'package:geolocator/geolocator.dart';

class LocationService {
  List<Position> _positions = [];
  
  Future<void> startTracking() async {
    final permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) return;
    
    Geolocator.getPositionStream(
      locationSettings: const LocationSettings(
        accuracy: LocationAccuracy.high,
        distanceFilter: 10,
      ),
    ).listen((position) {
      _positions.add(position);
    });
  }
  
  double calculateDistance() {
    double totalDistance = 0;
    for (int i = 0; i < _positions.length - 1; i++) {
      totalDistance += Geolocator.distanceBetween(
        _positions[i].latitude,
        _positions[i].longitude,
        _positions[i + 1].latitude,
        _positions[i + 1].longitude,
      );
    }
    return totalDistance / 1000; // 转换为公里
  }
}

4. 社交分享功能

使用share_plus分享运动成就:

dart 复制代码
import 'package:share_plus/share_plus.dart';

Future<void> shareAchievement(CheckInRecord record) async {
  final text = '''
我刚完成了一次${record.type.label}!
⏱️ 时长:${record.duration}分钟
🔥 消耗:${record.calories}千卡
${record.distance > 0 ? '📍 距离:${record.distance}公里\n' : ''}
一起来运动吧!
  ''';
  
  await Share.share(text);
}

5. 精确卡路里计算

根据运动类型和用户体重计算:

dart 复制代码
class CalorieCalculator {
  // MET值(代谢当量)
  static const Map<ExerciseType, double> metValues = {
    ExerciseType.running: 9.8,
    ExerciseType.cycling: 7.5,
    ExerciseType.swimming: 8.0,
    ExerciseType.yoga: 3.0,
    ExerciseType.gym: 6.0,
    ExerciseType.walking: 3.5,
    ExerciseType.basketball: 8.0,
    ExerciseType.football: 10.0,
  };
  
  // 计算消耗卡路里
  // 公式:卡路里 = MET × 体重(kg) × 时间(小时)
  static int calculate({
    required ExerciseType type,
    required int duration, // 分钟
    required double weight, // 公斤
  }) {
    final met = metValues[type] ?? 5.0;
    final hours = duration / 60;
    return (met * weight * hours).round();
  }
}

项目结构

复制代码
lib/
├── main.dart                    # 应用入口
├── models/                      # 数据模型
│   ├── check_in_record.dart    # 打卡记录
│   ├── fitness_goal.dart       # 运动目标
│   └── exercise_type.dart      # 运动类型
├── pages/                       # 页面
│   ├── home_page.dart          # 主页(底部导航)
│   ├── check_in_page.dart      # 打卡页面
│   ├── records_page.dart       # 记录页面
│   ├── stats_page.dart         # 统计页面
│   └── profile_page.dart       # 个人页面
├── services/                    # 服务
│   ├── data_service.dart       # 数据持久化
│   ├── notification_service.dart # 通知服务
│   ├── location_service.dart   # 定位服务
│   └── calorie_calculator.dart # 卡路里计算
└── widgets/                     # 组件
    ├── stat_card.dart          # 统计卡片
    ├── record_card.dart        # 记录卡片
    ├── goal_card.dart          # 目标卡片
    └── chart_widget.dart       # 图表组件

使用指南

快速开始

  1. 首次打卡

    • 打开应用进入打卡页面
    • 选择运动类型(如跑步)
    • 输入运动时长和距离
    • 点击"确认打卡"
  2. 查看记录

    • 切换到"记录"标签
    • 按日期浏览历史记录
    • 点击记录查看详情
  3. 查看统计

    • 切换到"统计"标签
    • 选择统计周期(周/月/年)
    • 查看数据图表和分析
  4. 设置目标

    • 进入"我的"页面
    • 点击"我的目标"
    • 设置运动目标

打卡技巧

最佳打卡时间:

  • 运动结束后立即打卡
  • 数据记忆更准确
  • 养成良好习惯

数据记录建议:

  • 如实记录运动数据
  • 添加备注记录感受
  • 定期回顾总结

坚持秘诀:

  • 设置合理目标
  • 开启运动提醒
  • 分享给好友监督

常见问题

Q1: 如何修改已打卡的记录?

目前版本暂不支持修改,建议打卡前仔细核对数据。后续版本会添加编辑功能。

Q2: 卡路里计算准确吗?

当前使用简化公式计算,实际消耗受多种因素影响。建议作为参考,不必过分追求精确。

Q3: 如何备份数据?

可以通过以下方式备份:

dart 复制代码
// 导出数据为JSON
Future<String> exportData() async {
  final records = await DataService().loadRecords();
  final jsonData = jsonEncode(records.map((r) => r.toJson()).toList());
  return jsonData;
}

// 导入数据
Future<void> importData(String jsonData) async {
  final jsonList = jsonDecode(jsonData) as List;
  final records = jsonList.map((json) => 
    CheckInRecord.fromJson(json)
  ).toList();
  await DataService().saveRecords(records);
}

Q4: 如何添加新的运动类型?

在ExerciseType枚举中添加:

dart 复制代码
enum ExerciseType {
  // 现有类型...
  climbing('爬山', Icons.terrain, Colors.brown),
  dancing('跳舞', Icons.music_note, Colors.pink),
}

Q5: 如何实现数据同步?

可以集成Firebase或自建后端:

dart 复制代码
import 'package:cloud_firestore/cloud_firestore.dart';

class SyncService {
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  
  Future<void> syncRecords(String userId, List<CheckInRecord> records) async {
    final batch = _firestore.batch();
    
    for (var record in records) {
      final docRef = _firestore
        .collection('users')
        .doc(userId)
        .collection('records')
        .doc(record.id);
      
      batch.set(docRef, {
        'type': record.type.name,
        'date': record.date,
        'duration': record.duration,
        'distance': record.distance,
        'calories': record.calories,
        'note': record.note,
      });
    }
    
    await batch.commit();
  }
}

性能优化

1. 列表优化

使用ListView.builder实现懒加载:

dart 复制代码
ListView.builder(
  itemCount: records.length,
  itemBuilder: (context, index) {
    return _buildRecordCard(records[index]);
  },
)

2. 图片缓存

如果添加用户头像,使用cached_network_image:

dart 复制代码
import 'package:cached_network_image/cached_network_image.dart';

CachedNetworkImage(
  imageUrl: userAvatarUrl,
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.person),
)

3. 状态管理优化

对于复杂应用,建议使用Provider或Riverpod:

dart 复制代码
import 'package:provider/provider.dart';

class RecordProvider extends ChangeNotifier {
  List<CheckInRecord> _records = [];
  
  List<CheckInRecord> get records => _records;
  
  void addRecord(CheckInRecord record) {
    _records.add(record);
    notifyListeners();
  }
  
  void removeRecord(String id) {
    _records.removeWhere((r) => r.id == id);
    notifyListeners();
  }
}

// 使用
ChangeNotifierProvider(
  create: (_) => RecordProvider(),
  child: MyApp(),
)

扩展功能建议

1. 运动计划

添加训练计划功能:

dart 复制代码
class TrainingPlan {
  final String id;
  final String name;
  final List<TrainingDay> days;
  final int weeks;
  
  TrainingPlan({
    required this.id,
    required this.name,
    required this.days,
    required this.weeks,
  });
}

class TrainingDay {
  final int dayNumber;
  final ExerciseType type;
  final int targetDuration;
  final String description;
  
  TrainingDay({
    required this.dayNumber,
    required this.type,
    required this.targetDuration,
    required this.description,
  });
}

2. 好友系统

添加好友功能,互相激励:

dart 复制代码
class Friend {
  final String id;
  final String name;
  final String avatar;
  final int continuousDays;
  final int totalDays;
  
  Friend({
    required this.id,
    required this.name,
    required this.avatar,
    required this.continuousDays,
    required this.totalDays,
  });
}

// 好友排行榜
Widget buildFriendRanking(List<Friend> friends) {
  friends.sort((a, b) => b.continuousDays.compareTo(a.continuousDays));
  
  return ListView.builder(
    itemCount: friends.length,
    itemBuilder: (context, index) {
      final friend = friends[index];
      return ListTile(
        leading: CircleAvatar(
          backgroundImage: NetworkImage(friend.avatar),
        ),
        title: Text(friend.name),
        subtitle: Text('连续${friend.continuousDays}天'),
        trailing: Text('#${index + 1}'),
      );
    },
  );
}

3. 成就徽章系统

dart 复制代码
class Achievement {
  final String id;
  final String name;
  final String description;
  final IconData icon;
  final Color color;
  final bool isUnlocked;
  final DateTime? unlockedDate;
  
  Achievement({
    required this.id,
    required this.name,
    required this.description,
    required this.icon,
    required this.color,
    required this.isUnlocked,
    this.unlockedDate,
  });
}

// 预定义成就
final achievements = [
  Achievement(
    id: 'first_check_in',
    name: '初次打卡',
    description: '完成第一次运动打卡',
    icon: Icons.star,
    color: Colors.yellow,
    isUnlocked: true,
  ),
  Achievement(
    id: 'week_warrior',
    name: '周冠军',
    description: '连续打卡7天',
    icon: Icons.emoji_events,
    color: Colors.orange,
    isUnlocked: true,
  ),
  Achievement(
    id: 'month_master',
    name: '月度大师',
    description: '连续打卡30天',
    icon: Icons.military_tech,
    color: Colors.red,
    isUnlocked: false,
  ),
];

4. 运动视频教程

集成视频播放功能:

dart 复制代码
import 'package:video_player/video_player.dart';

class ExerciseVideo {
  final String id;
  final String title;
  final String videoUrl;
  final ExerciseType type;
  final int duration;
  
  ExerciseVideo({
    required this.id,
    required this.title,
    required this.videoUrl,
    required this.type,
    required this.duration,
  });
}

class VideoPlayerPage extends StatefulWidget {
  final ExerciseVideo video;
  
  const VideoPlayerPage({required this.video});
  
  @override
  State<VideoPlayerPage> createState() => _VideoPlayerPageState();
}

class _VideoPlayerPageState extends State<VideoPlayerPage> {
  late VideoPlayerController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.network(widget.video.videoUrl)
      ..initialize().then((_) {
        setState(() {});
      });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.video.title)),
      body: Center(
        child: _controller.value.isInitialized
          ? AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: VideoPlayer(_controller),
            )
          : CircularProgressIndicator(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _controller.value.isPlaying
              ? _controller.pause()
              : _controller.play();
          });
        },
        child: Icon(
          _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
        ),
      ),
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

测试建议

单元测试

dart 复制代码
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('卡路里计算测试', () {
    test('跑步30分钟,体重60kg', () {
      final calories = CalorieCalculator.calculate(
        type: ExerciseType.running,
        duration: 30,
        weight: 60,
      );
      expect(calories, 294); // 9.8 * 60 * 0.5
    });
    
    test('瑜伽60分钟,体重50kg', () {
      final calories = CalorieCalculator.calculate(
        type: ExerciseType.yoga,
        duration: 60,
        weight: 50,
      );
      expect(calories, 150); // 3.0 * 50 * 1.0
    });
  });
  
  group('日期格式化测试', () {
    test('今天', () {
      final result = formatDate(DateTime.now());
      expect(result, '今天');
    });
    
    test('昨天', () {
      final yesterday = DateTime.now().subtract(Duration(days: 1));
      final result = formatDate(yesterday);
      expect(result, '昨天');
    });
  });
}

Widget测试

dart 复制代码
testWidgets('打卡按钮测试', (WidgetTester tester) async {
  await tester.pumpWidget(MaterialApp(home: CheckInPage()));
  
  // 查找跑步按钮
  final runningButton = find.text('跑步');
  expect(runningButton, findsOneWidget);
  
  // 点击按钮
  await tester.tap(runningButton);
  await tester.pumpAndSettle();
  
  // 验证对话框出现
  expect(find.text('运动时长(分钟)'), findsOneWidget);
});

发布准备

应用配置

pubspec.yaml:

yaml 复制代码
name: fitness_check_in
description: 运动健身打卡应用
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  shared_preferences: ^2.2.0
  flutter_local_notifications: ^16.0.0
  geolocator: ^10.0.0
  share_plus: ^7.0.0

应用图标

准备不同尺寸的图标,建议使用橙色运动主题。

权限配置

Android (AndroidManifest.xml):

xml 复制代码
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

iOS (Info.plist):

xml 复制代码
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限记录运动轨迹</string>

总结

运动健身打卡应用通过简洁的界面和完善的功能,帮助用户养成良好的运动习惯。主要特点包括:

核心价值

  1. 简单易用:快速打卡,操作便捷
  2. 数据可视化:直观的图表展示
  3. 激励机制:连续打卡、成就系统
  4. 目标管理:设定目标,追踪进度

技术亮点

  1. Material Design 3:现代化UI设计
  2. 模块化架构:代码结构清晰
  3. 数据持久化:本地存储支持
  4. 扩展性强:易于添加新功能

应用场景

  • 个人健身记录
  • 运动习惯养成
  • 健康数据管理
  • 运动社交分享

通过持续优化和功能扩展,这款应用可以成为用户运动健身的得力助手,帮助更多人享受运动带来的快乐和健康。


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

相关推荐
夜雨声烦丿2 小时前
Flutter 框架跨平台鸿蒙开发 - 动物识别工具应用开发教程
flutter·华为·harmonyos
IT陈图图2 小时前
基于 Flutter × OpenHarmony 音乐播放器应用 —— 构建搜索栏
flutter·开源·鸿蒙·openharmony
kirk_wang2 小时前
Flutter艺术探索-Flutter单元测试:test包使用指南
flutter·移动开发·flutter教程·移动开发教程
月未央2 小时前
鸿蒙版网易云音乐
华为·harmonyos
哈哈你是真的厉害2 小时前
小白基础入门 React Native 鸿蒙跨平台开发:实现一个简单的记账本小工具
react native·react.js·harmonyos
Swift社区2 小时前
DevEco Studio 调试鸿蒙应用常见问题解决方案
华为·harmonyos
AI_零食2 小时前
红蓝之辨:基于 Flutter 的动静脉血动力学可视化系统开发
flutter·ui·华为·harmonyos·鸿蒙
美狐美颜SDK开放平台3 小时前
跨平台开发实战:直播美颜sdk动态贴纸在 Android / iOS / HarmonyOS 的落地方案
android·ios·harmonyos·美颜sdk·直播美颜sdk·视频美颜sdk·美颜api
人工智能知识库3 小时前
华为HCCDA-GaussDB题库(带详细解析)
数据库·华为·gaussdb·题库·hccda-gaussdb·hccda