Flutter 框架跨平台鸿蒙开发 - 运动健身打卡:打造你的专属健身助手
运动健身打卡应用通过简洁的界面和完善的功能,帮助用户养成良好的运动习惯。
Flutter运动健身打卡:打造你的专属健身助手
项目简介
运动健身打卡是一款帮助用户养成运动习惯的移动应用,支持多种运动类型记录、数据统计分析、目标管理和成就系统。通过可视化的数据展示和激励机制,让运动变得更有趣、更有动力。
运行效果图




核心功能
- 多类型运动打卡:支持跑步、骑行、游泳、瑜伽等8种运动类型
- 数据统计分析:周/月/年维度的运动数据统计和可视化图表
- 连续打卡激励:显示连续打卡天数,激发坚持动力
- 目标管理:设置运动目标,追踪完成进度
- 成就系统:累计数据展示,记录运动里程碑
技术特点
| 技术 | 说明 |
|---|---|
| Flutter | 3.0+ |
| Material Design 3 | 现代化UI设计 |
| 底部导航 | NavigationBar组件 |
| 状态管理 | StatefulWidget |
| 数据可视化 | 自定义图表组件 |
功能架构
核心功能详解
1. 运动打卡页面
打卡页面是应用的核心,提供快速打卡入口和今日数据展示。
页面布局:
- 今日运动数据卡片(时长、消耗、次数)
- 连续打卡天数展示
- 8种运动类型网格
- 今日打卡记录列表
打卡流程:
- 选择运动类型
- 输入运动时长和距离
- 添加备注(可选)
- 确认打卡
- 自动计算消耗卡路里
卡路里计算公式:
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天数据
- 月统计:当月数据
- 年统计:全年数据
统计内容:
- 整体数据:运动天数、总时长、总消耗
- 趋势图表:每日运动时长柱状图
- 类型分布:各运动类型占比
- 目标进度:运动目标完成情况
柱状图实现:
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 # 图表组件
使用指南
快速开始
-
首次打卡
- 打开应用进入打卡页面
- 选择运动类型(如跑步)
- 输入运动时长和距离
- 点击"确认打卡"
-
查看记录
- 切换到"记录"标签
- 按日期浏览历史记录
- 点击记录查看详情
-
查看统计
- 切换到"统计"标签
- 选择统计周期(周/月/年)
- 查看数据图表和分析
-
设置目标
- 进入"我的"页面
- 点击"我的目标"
- 设置运动目标
打卡技巧
最佳打卡时间:
- 运动结束后立即打卡
- 数据记忆更准确
- 养成良好习惯
数据记录建议:
- 如实记录运动数据
- 添加备注记录感受
- 定期回顾总结
坚持秘诀:
- 设置合理目标
- 开启运动提醒
- 分享给好友监督
常见问题
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>
总结
运动健身打卡应用通过简洁的界面和完善的功能,帮助用户养成良好的运动习惯。主要特点包括:
核心价值
- 简单易用:快速打卡,操作便捷
- 数据可视化:直观的图表展示
- 激励机制:连续打卡、成就系统
- 目标管理:设定目标,追踪进度
技术亮点
- Material Design 3:现代化UI设计
- 模块化架构:代码结构清晰
- 数据持久化:本地存储支持
- 扩展性强:易于添加新功能
应用场景
- 个人健身记录
- 运动习惯养成
- 健康数据管理
- 运动社交分享
通过持续优化和功能扩展,这款应用可以成为用户运动健身的得力助手,帮助更多人享受运动带来的快乐和健康。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)