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

项目简介

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

核心功能

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

技术特点

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

功能架构

运动健身打卡

打卡页面

记录页面

统计页面

个人页面

今日数据

连续打卡

运动类型选择

打卡记录

按日期分组

记录详情

搜索功能

周期选择

整体统计

趋势图表

类型分布

目标进度

用户信息

运动成就

功能设置

核心功能详解

1. 运动打卡页面

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

页面布局:

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

打卡流程:

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

卡路里计算公式:

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

2. 运动记录页面

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

数据分组:

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

记录信息:

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

日期格式化:

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. 目标进度:运动目标完成情况

柱状图实现:

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. 个人中心页面

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

用户信息:

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

运动成就:

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

功能菜单:

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

数据模型设计

运动类型枚举

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);
}

打卡记录模型

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 = '',
  });
}

运动目标模型

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;
}

核心代码实现

打卡对话框

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('确认打卡'),
        ),
      ],
    ),
  );
}

记录分组显示

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)),
    ],
  );
}

统计数据计算

// 计算周期内的统计数据
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,
  };
}

目标进度展示

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组件:

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组件,保持视觉一致性:

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

3. 网格布局

运动类型使用4列网格:

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保存数据:

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实现定时提醒:

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实现跑步轨迹记录:

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分享运动成就:

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. 精确卡路里计算

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

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: 如何备份数据?

可以通过以下方式备份:

// 导出数据为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枚举中添加:

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

Q5: 如何实现数据同步?

可以集成Firebase或自建后端:

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实现懒加载:

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

2. 图片缓存

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

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:

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. 运动计划

添加训练计划功能:

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. 好友系统

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

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. 成就徽章系统

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. 运动视频教程

集成视频播放功能:

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});
  
  
  State<VideoPlayerPage> createState() => _VideoPlayerPageState();
}

class _VideoPlayerPageState extends State<VideoPlayerPage> {
  late VideoPlayerController _controller;
  
  
  void initState() {
    super.initState();
    _controller = VideoPlayerController.network(widget.video.videoUrl)
      ..initialize().then((_) {
        setState(() {});
      });
  }
  
  
  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,
        ),
      ),
    );
  }
  
  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

测试建议

单元测试

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测试

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:

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):

<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):

<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限记录运动轨迹</string>

总结

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

核心价值

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

技术亮点

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

应用场景

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

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


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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐