Flutter 框架跨平台鸿蒙开发 - 健身训练计划开发教程
健身训练计划是一款专业的健身管理应用,帮助用户制定个性化训练计划、追踪训练进度、记录健身成果。应用采用现代化的Material Design 3设计语言,提供直观友好的用户界面和完整的健身管理功能。运行效果图FitnessTrainingAppFitnessTrainingHomePage今日训练页面训练计划页面训练进度页面设置页面数据模型层TrainingPlanExerciseTraining
Flutter健身训练计划开发教程
项目简介
健身训练计划是一款专业的健身管理应用,帮助用户制定个性化训练计划、追踪训练进度、记录健身成果。应用采用现代化的Material Design 3设计语言,提供直观友好的用户界面和完整的健身管理功能。
运行效果图




核心功能特性
- 今日训练管理:实时显示当日训练计划,支持快速开始训练
- 训练计划制定:创建个性化训练计划,包含多种训练类型和难度级别
- 训练进度追踪:详细记录每次训练情况,支持评分和备注
- 智能统计分析:训练数据统计、卡路里消耗、连续训练天数等
- 个性化设置:训练提醒、身体数据管理、数据备份等功能
技术架构特点
- 响应式设计:适配不同屏幕尺寸,提供一致的用户体验
- 状态管理:使用StatefulWidget进行本地状态管理
- 数据模型:完整的数据结构设计,支持复杂的健身业务逻辑
- 动画效果:流畅的页面切换和交互动画
- 模块化架构:清晰的代码结构,便于维护和扩展
项目架构设计
整体架构图
数据流架构
数据模型设计
核心数据结构
1. 训练计划模型(TrainingPlan)
class TrainingPlan {
final String id; // 唯一标识
final String name; // 计划名称
final String description; // 计划描述
final TrainingLevel level; // 训练难度
final TrainingType type; // 训练类型
final int durationWeeks; // 持续周数
final int sessionsPerWeek; // 每周训练次数
final List<String> targetMuscles; // 目标肌群
final List<Exercise> exercises; // 包含的练习
final String imageUrl; // 计划图片
final DateTime createdDate; // 创建日期
final bool isActive; // 是否激活
final String notes; // 备注
final int estimatedCalories; // 预计消耗卡路里
final int estimatedDuration; // 预计训练时长(分钟)
}
2. 运动模型(Exercise)
class Exercise {
final String id; // 练习ID
final String name; // 练习名称
final String description; // 练习描述
final ExerciseType type; // 练习类型
final MuscleGroup primaryMuscle; // 主要肌群
final List<MuscleGroup> secondaryMuscles; // 次要肌群
final EquipmentType equipment; // 所需器械
final DifficultyLevel difficulty; // 难度等级
final int sets; // 组数
final int reps; // 每组次数
final int restSeconds; // 休息时间(秒)
final double weight; // 重量(kg)
final int duration; // 持续时间(秒,用于有氧运动)
final String instructions; // 动作要领
final List<String> tips; // 训练技巧
final String videoUrl; // 视频链接
final String imageUrl; // 图片链接
final int caloriesPerRep; // 每次消耗卡路里
}
3. 训练记录模型(TrainingRecord)
class TrainingRecord {
final String id; // 记录ID
final String planId; // 关联计划ID
final String exerciseId; // 关联练习ID
final DateTime date; // 训练日期
final int completedSets; // 完成组数
final int completedReps; // 完成次数
final double actualWeight; // 实际重量
final int actualDuration; // 实际时长
final int caloriesBurned; // 消耗卡路里
final double rating; // 感受评分(1-5星)
final String notes; // 备注
final TrainingStatus status; // 训练状态
final DateTime startTime; // 开始时间
final DateTime? endTime; // 结束时间
}
枚举类型定义
训练难度枚举
enum TrainingLevel {
beginner, // 初级
intermediate, // 中级
advanced, // 高级
expert, // 专家级
}
训练类型枚举
enum TrainingType {
strength, // 力量训练
cardio, // 有氧训练
flexibility, // 柔韧性训练
mixed, // 混合训练
hiit, // 高强度间歇训练
yoga, // 瑜伽
pilates, // 普拉提
}
练习类型枚举
enum ExerciseType {
strength, // 力量
cardio, // 有氧
flexibility, // 柔韧
balance, // 平衡
plyometric, // 爆发力
}
肌群分类枚举
enum MuscleGroup {
chest, // 胸部
back, // 背部
shoulders, // 肩部
arms, // 手臂
core, // 核心
legs, // 腿部
glutes, // 臀部
calves, // 小腿
fullBody, // 全身
}
器械类型枚举
enum EquipmentType {
none, // 无器械
dumbbells, // 哑铃
barbell, // 杠铃
kettlebell, // 壶铃
resistance, // 阻力带
machine, // 器械
cardio, // 有氧器械
mat, // 瑜伽垫
}
训练状态枚举
enum TrainingStatus {
planned, // 计划中
inProgress, // 进行中
completed, // 已完成
skipped, // 跳过
failed, // 失败
}
核心功能实现
1. 今日训练管理
今日训练页面是应用的核心功能,提供当日训练计划的完整视图和快速操作入口。
页面结构设计
Widget _buildTodayPage() {
return Column(
children: [
_buildTodayHeader(), // 顶部统计卡片
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildTodayStats(), // 今日概览统计
_buildTodayWorkout(), // 训练计划展示
_buildQuickActions(), // 快速操作面板
],
),
),
),
],
);
}
统计数据计算
void _updateStatistics() {
setState(() {
_totalPlans = _trainingPlans.length;
_activePlans = _trainingPlans.where((p) => p.isActive).length;
_completedWorkouts = _records.where((r) => r.status == TrainingStatus.completed).length;
_totalCaloriesBurned = _records.fold(0, (sum, r) => sum + r.caloriesBurned);
final completedRecords = _records.where((r) => r.status == TrainingStatus.completed).toList();
if (completedRecords.isNotEmpty) {
_averageRating = completedRecords.fold(0.0, (sum, r) => sum + r.rating) / completedRecords.length;
}
_currentStreak = _calculateCurrentStreak();
});
}
连续训练天数计算
int _calculateCurrentStreak() {
final sortedRecords = _records
.where((r) => r.status == TrainingStatus.completed)
.toList()
..sort((a, b) => b.date.compareTo(a.date));
if (sortedRecords.isEmpty) return 0;
int streak = 0;
DateTime currentDate = DateTime.now();
for (final record in sortedRecords) {
final recordDate = DateTime(record.date.year, record.date.month, record.date.day);
final checkDate = DateTime(currentDate.year, currentDate.month, currentDate.day);
if (recordDate == checkDate || recordDate == checkDate.subtract(const Duration(days: 1))) {
streak++;
currentDate = record.date.subtract(const Duration(days: 1));
} else {
break;
}
}
return streak;
}
今日训练展示
Widget _buildTodayWorkout() {
final todayRecords = _getTodayRecords();
final activePlan = _getActivePlan();
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('今日训练', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
if (activePlan != null)
TextButton(
onPressed: () => _showPlanDetails(activePlan),
child: const Text('查看计划'),
),
],
),
if (activePlan == null)
_buildEmptyState()
else ...[
_buildPlanSummary(activePlan),
const SizedBox(height: 16),
const Text('今日练习', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
...activePlan.exercises.take(3).map((exercise) {
final record = todayRecords.firstWhere(
(r) => r.exerciseId == exercise.id,
orElse: () => _createDefaultRecord(activePlan.id, exercise.id),
);
return _buildExerciseItem(exercise, record);
}),
],
],
),
),
);
}
2. 训练计划管理
训练计划功能提供完整的计划创建、编辑和管理功能。
计划列表展示
Widget _buildPlanCard(TrainingPlan plan) {
return Card(
child: InkWell(
onTap: () => _showPlanDetails(plan),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// 计划图标
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: _getTrainingTypeColor(plan.type).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(_getTrainingTypeIcon(plan.type)),
),
const SizedBox(width: 16),
// 计划信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(plan.name, style: TextStyle(fontWeight: FontWeight.bold)),
Text('${plan.estimatedDuration}分钟 • ${plan.estimatedCalories}卡路里'),
Text(plan.description, maxLines: 1, overflow: TextOverflow.ellipsis),
],
),
),
],
),
const SizedBox(height: 12),
// 标签和操作按钮
Row(
children: [
_buildLevelChip(plan.level),
const SizedBox(width: 8),
_buildTypeChip(plan.type),
const Spacer(),
if (plan.isActive)
ElevatedButton(
onPressed: () => _startWorkout(plan),
child: const Text('开始训练'),
),
],
),
],
),
),
),
);
}
添加计划对话框
class _AddPlanDialog extends StatefulWidget {
final Function(TrainingPlan) onSave;
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('创建训练计划'),
content: SizedBox(
width: 400,
height: 600,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
children: [
// 计划名称输入
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: '计划名称 *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.fitness_center),
),
validator: (value) => value?.isEmpty ?? true ? '请输入计划名称' : null,
),
// 计划描述输入
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: '计划描述 *',
border: OutlineInputBorder(),
),
maxLines: 2,
validator: (value) => value?.isEmpty ?? true ? '请输入计划描述' : null,
),
// 难度和类型选择
Row(
children: [
Expanded(
child: DropdownButtonFormField<TrainingLevel>(
value: _level,
decoration: const InputDecoration(labelText: '训练难度'),
items: TrainingLevel.values.map((level) =>
DropdownMenuItem(value: level, child: Text(_getTrainingLevelName(level)))).toList(),
onChanged: (value) => setState(() => _level = value!),
),
),
Expanded(
child: DropdownButtonFormField<TrainingType>(
value: _type,
decoration: const InputDecoration(labelText: '训练类型'),
items: TrainingType.values.map((type) =>
DropdownMenuItem(value: type, child: Text(_getTrainingTypeName(type)))).toList(),
onChanged: (value) => setState(() => _type = value!),
),
),
],
),
// 持续时间和频率设置
Row(
children: [
Expanded(
child: TextFormField(
initialValue: _durationWeeks.toString(),
decoration: const InputDecoration(labelText: '持续周数', suffixText: '周'),
keyboardType: TextInputType.number,
onChanged: (value) => _durationWeeks = int.tryParse(value) ?? 4,
),
),
Expanded(
child: TextFormField(
initialValue: _sessionsPerWeek.toString(),
decoration: const InputDecoration(labelText: '每周次数', suffixText: '次'),
keyboardType: TextInputType.number,
onChanged: (value) => _sessionsPerWeek = int.tryParse(value) ?? 3,
),
),
],
),
],
),
),
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消')),
ElevatedButton(onPressed: _savePlan, child: Text('保存')),
],
);
}
}
3. 训练执行功能
训练执行功能提供完整的训练过程管理,包括练习指导、进度追踪和结果记录。
训练对话框
class _WorkoutDialog extends StatefulWidget {
final TrainingPlan plan;
final Function(List<TrainingRecord>) onComplete;
Widget build(BuildContext context) {
return AlertDialog(
title: Text('训练:${widget.plan.name}'),
content: SizedBox(
width: 400,
height: 400,
child: Column(
children: [
// 进度条
LinearProgressIndicator(
value: (_currentExerciseIndex + 1) / widget.plan.exercises.length,
),
const SizedBox(height: 16),
Text('第${_currentExerciseIndex + 1}/${widget.plan.exercises.length}个练习'),
const SizedBox(height: 16),
// 当前练习展示
if (widget.plan.exercises.isNotEmpty) ...[
_buildCurrentExercise(),
const SizedBox(height: 16),
// 休息计时器或练习控制
if (_isResting)
_buildRestTimer()
else
_buildExerciseControls(),
],
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('退出训练')),
if (_currentExerciseIndex >= widget.plan.exercises.length - 1)
ElevatedButton(onPressed: _completeWorkout, child: Text('完成训练')),
],
);
}
}
休息计时器
Widget _buildRestTimer() {
return Column(
children: [
const Text('休息时间', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Text('$_restTimeRemaining秒', style: TextStyle(fontSize: 32, color: Colors.orange)),
const SizedBox(height: 16),
ElevatedButton(onPressed: _skipRest, child: Text('跳过休息')),
],
);
}
void _startRestTimer() {
Future.delayed(const Duration(seconds: 1), () {
if (_isResting && _restTimeRemaining > 0) {
setState(() => _restTimeRemaining--);
_startRestTimer();
} else if (_isResting) {
_skipRest();
}
});
}
练习完成记录
void _completeCurrentExercise() {
final exercise = widget.plan.exercises[_currentExerciseIndex];
final record = TrainingRecord(
id: DateTime.now().millisecondsSinceEpoch.toString(),
planId: widget.plan.id,
exerciseId: exercise.id,
date: DateTime.now(),
completedSets: exercise.sets,
completedReps: exercise.reps * exercise.sets,
actualDuration: exercise.duration,
caloriesBurned: exercise.caloriesPerRep * exercise.reps * exercise.sets,
rating: 4.0,
status: TrainingStatus.completed,
startTime: DateTime.now().subtract(const Duration(minutes: 5)),
endTime: DateTime.now(),
);
_records.add(record);
if (_currentExerciseIndex < widget.plan.exercises.length - 1) {
setState(() {
_isResting = true;
_restTimeRemaining = exercise.restSeconds;
});
_startRestTimer();
} else {
_completeWorkout();
}
}
4. 训练进度追踪
训练进度功能提供详细的训练数据统计和可视化分析。
进度统计计算
int _getWeeklyWorkouts() {
final now = DateTime.now();
final weekStart = now.subtract(Duration(days: now.weekday - 1));
return _records
.where((r) =>
r.status == TrainingStatus.completed &&
r.date.isAfter(weekStart) &&
r.date.isBefore(now.add(const Duration(days: 1))))
.length;
}
int _getAverageWorkoutDuration() {
final completedRecords = _records
.where((r) => r.status == TrainingStatus.completed && r.endTime != null)
.toList();
if (completedRecords.isEmpty) return 0;
final totalMinutes = completedRecords.fold(0, (sum, r) {
final duration = r.endTime!.difference(r.startTime).inMinutes;
return sum + duration;
});
return totalMinutes ~/ completedRecords.length;
}
int _getLongestStreak() {
final sortedRecords = _records
.where((r) => r.status == TrainingStatus.completed)
.toList()
..sort((a, b) => a.date.compareTo(b.date));
if (sortedRecords.isEmpty) return 0;
int maxStreak = 1;
int currentStreak = 1;
DateTime lastDate = sortedRecords.first.date;
for (int i = 1; i < sortedRecords.length; i++) {
final currentDate = sortedRecords[i].date;
final daysDiff = currentDate.difference(lastDate).inDays;
if (daysDiff == 1) {
currentStreak++;
maxStreak = maxStreak > currentStreak ? maxStreak : currentStreak;
} else if (daysDiff > 1) {
currentStreak = 1;
}
lastDate = currentDate;
}
return maxStreak;
}
进度数据展示
Widget _buildProgressStats() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('统计数据', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.5,
children: [
_buildProgressStatCard('总训练次数', '$_completedWorkouts', Icons.fitness_center, Colors.orange, '次'),
_buildProgressStatCard('总卡路里', '$_totalCaloriesBurned', Icons.whatshot, Colors.red, 'kcal'),
_buildProgressStatCard('平均时长', '${_getAverageWorkoutDuration()}', Icons.schedule, Colors.blue, '分钟'),
_buildProgressStatCard('最长连续', '${_getLongestStreak()}', Icons.local_fire_department, Colors.purple, '天'),
],
),
],
),
),
);
}
训练记录展示
Widget _buildRecordItem(TrainingRecord record, Exercise exercise) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _getStatusColor(record.status).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _getStatusColor(record.status).withValues(alpha: 0.3)),
),
child: Row(
children: [
// 练习图标
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _getMuscleGroupColor(exercise.primaryMuscle).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(_getMuscleGroupIcon(exercise.primaryMuscle)),
),
const SizedBox(width: 16),
// 记录信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(exercise.name, style: TextStyle(fontWeight: FontWeight.bold)),
Text('${_formatDateTime(record.date)} • ${record.caloriesBurned}卡路里'),
if (record.notes.isNotEmpty)
Text(record.notes, style: TextStyle(fontStyle: FontStyle.italic), maxLines: 1),
],
),
),
// 状态和评分
Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getStatusColor(record.status),
borderRadius: BorderRadius.circular(12),
),
child: Text(_getStatusName(record.status),
style: TextStyle(color: Colors.white, fontSize: 10)),
),
if (record.rating > 0)
Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(5, (index) => Icon(
index < record.rating ? Icons.star : Icons.star_border,
size: 12, color: Colors.amber,
)),
),
],
),
],
),
);
}
5. 搜索与筛选功能
应用提供强大的搜索和筛选功能,帮助用户快速找到所需的训练计划。
搜索功能实现
Widget _buildSearchAndFilter() {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 搜索框
TextField(
decoration: const InputDecoration(
hintText: '搜索训练计划...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onChanged: (value) => setState(() => _searchQuery = value),
),
const SizedBox(height: 12),
// 筛选选项
Row(
children: [
Expanded(child: _buildFilterChip('类型', _getTypeFilterName(), _showTypeFilter)),
Expanded(child: _buildFilterChip('难度', _getLevelFilterName(), _showLevelFilter)),
Expanded(child: _buildFilterChip('排序', _getSortName(_sortBy), _showSortOptions)),
],
),
],
),
);
}
数据筛选逻辑
List<TrainingPlan> _getFilteredPlans() {
var filtered = _trainingPlans.where((plan) {
// 搜索过滤
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
if (!plan.name.toLowerCase().contains(query) &&
!plan.description.toLowerCase().contains(query)) {
return false;
}
}
// 类型过滤
if (_selectedTypes.isNotEmpty) {
if (!_selectedTypes.contains(plan.type)) {
return false;
}
}
// 难度过滤
if (_selectedLevels.isNotEmpty) {
if (!_selectedLevels.contains(plan.level)) {
return false;
}
}
return true;
}).toList();
// 排序处理
filtered.sort((a, b) {
switch (_sortBy) {
case SortBy.name:
return _sortAscending ? a.name.compareTo(b.name) : b.name.compareTo(a.name);
case SortBy.date:
return _sortAscending ? a.createdDate.compareTo(b.createdDate) : b.createdDate.compareTo(a.createdDate);
case SortBy.level:
return _sortAscending ? a.level.index.compareTo(b.level.index) : b.level.index.compareTo(a.level.index);
case SortBy.duration:
return _sortAscending ? a.estimatedDuration.compareTo(b.estimatedDuration) : b.estimatedDuration.compareTo(a.estimatedDuration);
case SortBy.calories:
return _sortAscending ? a.estimatedCalories.compareTo(b.estimatedCalories) : b.estimatedCalories.compareTo(a.estimatedCalories);
}
});
return filtered;
}
筛选对话框
void _showTypeFilter() {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text('选择训练类型'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: TrainingType.values.map((type) {
return CheckboxListTile(
title: Text(_getTrainingTypeName(type)),
value: _selectedTypes.contains(type),
onChanged: (checked) {
setState(() {
if (checked == true) {
_selectedTypes.add(type);
} else {
_selectedTypes.remove(type);
}
});
},
);
}).toList(),
),
actions: [
TextButton(onPressed: () => setState(() => _selectedTypes.clear()), child: Text('清空')),
TextButton(onPressed: () => Navigator.pop(context), child: Text('取消')),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
this.setState(() {});
},
child: Text('确定'),
),
],
);
},
);
},
);
}
6. 设置管理功能
设置页面提供个性化配置和数据管理功能。
设置页面结构
Widget _buildSettingsPage() {
return Column(
children: [
_buildSettingsHeader(),
Expanded(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildTrainingSettings(), // 训练设置
_buildBodyDataSettings(), // 身体数据
_buildDataManagement(), // 数据管理
_buildAboutSection(), // 关于应用
],
),
),
],
);
}
训练设置
Widget _buildTrainingSettings() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('训练设置', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
ListTile(
leading: const Icon(Icons.schedule),
title: const Text('默认休息时间'),
subtitle: const Text('60秒'),
onTap: _showRestTimeSettings,
),
ListTile(
leading: const Icon(Icons.notifications),
title: const Text('训练提醒'),
subtitle: const Text('每日20:00'),
onTap: _showReminderSettings,
),
SwitchListTile(
secondary: const Icon(Icons.vibration),
title: const Text('振动反馈'),
subtitle: const Text('完成动作时振动'),
value: _vibrationEnabled,
onChanged: (value) => setState(() => _vibrationEnabled = value),
),
],
),
),
);
}
身体数据管理
Widget _buildBodyDataSettings() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('身体数据', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
ListTile(
leading: const Icon(Icons.person),
title: const Text('个人信息'),
subtitle: const Text('身高、体重、年龄'),
onTap: _showPersonalInfo,
),
ListTile(
leading: const Icon(Icons.track_changes),
title: const Text('健身目标'),
subtitle: const Text('减脂、增肌、塑形'),
onTap: _showFitnessGoals,
),
ListTile(
leading: const Icon(Icons.monitor_weight),
title: const Text('体重记录'),
subtitle: const Text('追踪体重变化'),
onTap: _showWeightTracking,
),
],
),
),
);
}
UI组件设计
1. 导航栏设计
应用采用底部导航栏设计,提供四个主要功能模块的快速切换。
NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
destinations: const [
NavigationDestination(
icon: Icon(Icons.today_outlined),
selectedIcon: Icon(Icons.today),
label: '今日训练',
),
NavigationDestination(
icon: Icon(Icons.fitness_center_outlined),
selectedIcon: Icon(Icons.fitness_center),
label: '训练计划',
),
NavigationDestination(
icon: Icon(Icons.trending_up_outlined),
selectedIcon: Icon(Icons.trending_up),
label: '训练进度',
),
NavigationDestination(
icon: Icon(Icons.settings_outlined),
selectedIcon: Icon(Icons.settings),
label: '设置',
),
],
)
2. 卡片组件设计
统计卡片
Widget _buildStatCard(String title, String value, IconData icon, Color color, String unit) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(value, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: color)),
Text(unit, style: TextStyle(fontSize: 12, color: color)),
const SizedBox(height: 4),
Text(title, style: TextStyle(fontSize: 12, color: Colors.grey.shade600), textAlign: TextAlign.center),
],
),
);
}
训练计划卡片
Widget _buildPlanSummary(TrainingPlan plan) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange.shade200),
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(_getTrainingTypeIcon(plan.type), size: 30, color: Colors.orange.shade600),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(plan.name, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text('${plan.estimatedDuration}分钟 • ${plan.estimatedCalories}卡路里'),
Text(_getTrainingLevelName(plan.level),
style: TextStyle(fontSize: 12, color: Colors.orange.shade600)),
],
),
),
ElevatedButton(
onPressed: () => _startWorkout(plan),
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange, foregroundColor: Colors.white),
child: const Text('开始训练'),
),
],
),
);
}
3. 状态指示器
训练状态指示器
Widget _buildStatusIndicator(TrainingStatus status) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getStatusColor(status),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_getStatusName(status),
style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.w500),
),
);
}
难度等级指示器
Widget _buildLevelChip(TrainingLevel level) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _getTrainingLevelColor(level).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_getTrainingLevelName(level),
style: TextStyle(fontSize: 10, color: _getTrainingLevelColor(level)),
),
);
}
4. 动画效果
页面切换动画
class _FitnessTrainingHomePageState extends State<FitnessTrainingHomePage>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _scaleController;
void initState() {
super.initState();
_fadeController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_fadeController.forward();
}
Widget _buildAnimatedPage(Widget child) {
return FadeTransition(
opacity: _fadeController,
child: ScaleTransition(
scale: Tween<double>(begin: 0.95, end: 1.0).animate(
CurvedAnimation(parent: _scaleController, curve: Curves.easeOut),
),
child: child,
),
);
}
}
进度条动画
Widget _buildProgressIndicator(double progress) {
return TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 500),
tween: Tween<double>(begin: 0.0, end: progress),
builder: (context, value, child) {
return LinearProgressIndicator(
value: value,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(Colors.orange),
);
},
);
}
工具方法实现
1. 日期时间处理
日期格式化
String _formatDate(DateTime date) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final targetDate = DateTime(date.year, date.month, date.day);
if (targetDate == today) {
return '今天';
} else if (targetDate == today.subtract(const Duration(days: 1))) {
return '昨天';
} else if (targetDate == today.add(const Duration(days: 1))) {
return '明天';
} else {
return '${date.month}月${date.day}日';
}
}
String _formatDateTime(DateTime dateTime) {
return '${_formatDate(dateTime)} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
时间计算
bool _isToday(DateTime date) {
final now = DateTime.now();
return date.year == now.year && date.month == now.month && date.day == now.day;
}
Duration _getWorkoutDuration(TrainingRecord record) {
if (record.endTime != null) {
return record.endTime!.difference(record.startTime);
}
return Duration.zero;
}
int _getWeekOfYear(DateTime date) {
final firstDayOfYear = DateTime(date.year, 1, 1);
final daysSinceFirstDay = date.difference(firstDayOfYear).inDays;
return (daysSinceFirstDay / 7).ceil();
}
2. 状态管理工具
状态颜色映射
Color _getStatusColor(TrainingStatus status) {
switch (status) {
case TrainingStatus.planned:
return Colors.blue;
case TrainingStatus.inProgress:
return Colors.orange;
case TrainingStatus.completed:
return Colors.green;
case TrainingStatus.skipped:
return Colors.grey;
case TrainingStatus.failed:
return Colors.red;
}
}
String _getStatusName(TrainingStatus status) {
switch (status) {
case TrainingStatus.planned:
return '计划中';
case TrainingStatus.inProgress:
return '进行中';
case TrainingStatus.completed:
return '已完成';
case TrainingStatus.skipped:
return '跳过';
case TrainingStatus.failed:
return '失败';
}
}
训练类型管理
String _getTrainingTypeName(TrainingType type) {
const typeNames = {
TrainingType.strength: '力量训练',
TrainingType.cardio: '有氧训练',
TrainingType.flexibility: '柔韧性',
TrainingType.mixed: '混合训练',
TrainingType.hiit: 'HIIT',
TrainingType.yoga: '瑜伽',
TrainingType.pilates: '普拉提',
};
return typeNames[type] ?? '未知';
}
Color _getTrainingTypeColor(TrainingType type) {
const typeColors = {
TrainingType.strength: Colors.red,
TrainingType.cardio: Colors.blue,
TrainingType.flexibility: Colors.green,
TrainingType.mixed: Colors.orange,
TrainingType.hiit: Colors.purple,
TrainingType.yoga: Colors.teal,
TrainingType.pilates: Colors.pink,
};
return typeColors[type] ?? Colors.grey;
}
IconData _getTrainingTypeIcon(TrainingType type) {
const typeIcons = {
TrainingType.strength: Icons.fitness_center,
TrainingType.cardio: Icons.directions_run,
TrainingType.flexibility: Icons.self_improvement,
TrainingType.mixed: Icons.sports_gymnastics,
TrainingType.hiit: Icons.flash_on,
TrainingType.yoga: Icons.spa,
TrainingType.pilates: Icons.accessibility_new,
};
return typeIcons[type] ?? Icons.fitness_center;
}
3. 数据验证工具
输入验证
class ValidationUtils {
static String? validatePlanName(String? value) {
if (value == null || value.trim().isEmpty) {
return '请输入计划名称';
}
if (value.trim().length < 2) {
return '计划名称至少需要2个字符';
}
return null;
}
static String? validateDuration(String? value) {
if (value == null || value.trim().isEmpty) {
return '请输入时长';
}
final duration = int.tryParse(value.trim());
if (duration == null || duration <= 0) {
return '请输入有效的时长';
}
if (duration > 300) {
return '时长不能超过300分钟';
}
return null;
}
static String? validateCalories(String? value) {
if (value != null && value.trim().isNotEmpty) {
final calories = int.tryParse(value.trim());
if (calories == null || calories < 0) {
return '请输入有效的卡路里数值';
}
if (calories > 2000) {
return '卡路里数值过大';
}
}
return null;
}
static String? validateWeight(String? value) {
if (value != null && value.trim().isNotEmpty) {
final weight = double.tryParse(value.trim());
if (weight == null || weight < 0) {
return '请输入有效的重量';
}
if (weight > 500) {
return '重量数值过大';
}
}
return null;
}
}
数据完整性检查
bool _isPlanDataComplete(TrainingPlan plan) {
return plan.name.isNotEmpty &&
plan.description.isNotEmpty &&
plan.exercises.isNotEmpty &&
plan.estimatedDuration > 0;
}
bool _isRecordValid(TrainingRecord record) {
return record.exerciseId.isNotEmpty &&
record.date.isBefore(DateTime.now().add(const Duration(days: 1))) &&
record.caloriesBurned >= 0;
}
bool _isExerciseDataComplete(Exercise exercise) {
return exercise.name.isNotEmpty &&
exercise.description.isNotEmpty &&
exercise.sets > 0 &&
(exercise.reps > 0 || exercise.duration > 0);
}
4. 数据处理工具
统计计算
class FitnessStatistics {
static double calculateBMI(double weight, double height) {
if (height <= 0) return 0.0;
return weight / (height * height);
}
static int calculateCaloriesBurned(Exercise exercise, int completedReps, int completedSets) {
return exercise.caloriesPerRep * completedReps * completedSets;
}
static double calculateAverageRating(List<TrainingRecord> records) {
final ratedRecords = records.where((r) => r.rating > 0).toList();
if (ratedRecords.isEmpty) return 0.0;
final totalRating = ratedRecords.fold(0.0, (sum, r) => sum + r.rating);
return totalRating / ratedRecords.length;
}
static Map<TrainingType, int> getTrainingTypeDistribution(List<TrainingPlan> plans) {
final distribution = <TrainingType, int>{};
for (final type in TrainingType.values) {
distribution[type] = plans.where((plan) => plan.type == type).length;
}
return distribution;
}
static List<TrainingPlan> getMostActiveWeekPlans(List<TrainingRecord> records) {
final weeklyActivity = <String, int>{};
for (final record in records) {
if (record.status == TrainingStatus.completed) {
final weekKey = '${record.date.year}-W${_getWeekOfYear(record.date)}';
weeklyActivity[weekKey] = (weeklyActivity[weekKey] ?? 0) + 1;
}
}
// 返回最活跃周的计划(这里简化处理)
return [];
}
}
数据排序
class SortingUtils {
static List<TrainingPlan> sortPlans(List<TrainingPlan> plans, SortBy sortBy, bool ascending) {
final sorted = List<TrainingPlan>.from(plans);
sorted.sort((a, b) {
int comparison;
switch (sortBy) {
case SortBy.name:
comparison = a.name.compareTo(b.name);
break;
case SortBy.date:
comparison = a.createdDate.compareTo(b.createdDate);
break;
case SortBy.level:
comparison = a.level.index.compareTo(b.level.index);
break;
case SortBy.type:
comparison = a.type.index.compareTo(b.type.index);
break;
case SortBy.duration:
comparison = a.estimatedDuration.compareTo(b.estimatedDuration);
break;
case SortBy.calories:
comparison = a.estimatedCalories.compareTo(b.estimatedCalories);
break;
}
return ascending ? comparison : -comparison;
});
return sorted;
}
static List<TrainingRecord> sortRecords(List<TrainingRecord> records, bool byDateDescending) {
final sorted = List<TrainingRecord>.from(records);
sorted.sort((a, b) => byDateDescending
? b.date.compareTo(a.date)
: a.date.compareTo(b.date));
return sorted;
}
}
5. 本地存储工具
数据序列化
class DataSerializer {
static Map<String, dynamic> planToJson(TrainingPlan plan) {
return {
'id': plan.id,
'name': plan.name,
'description': plan.description,
'level': plan.level.index,
'type': plan.type.index,
'durationWeeks': plan.durationWeeks,
'sessionsPerWeek': plan.sessionsPerWeek,
'targetMuscles': plan.targetMuscles,
'exercises': plan.exercises.map((e) => exerciseToJson(e)).toList(),
'imageUrl': plan.imageUrl,
'createdDate': plan.createdDate.toIso8601String(),
'isActive': plan.isActive,
'notes': plan.notes,
'estimatedCalories': plan.estimatedCalories,
'estimatedDuration': plan.estimatedDuration,
};
}
static TrainingPlan planFromJson(Map<String, dynamic> json) {
return TrainingPlan(
id: json['id'],
name: json['name'],
description: json['description'],
level: TrainingLevel.values[json['level']],
type: TrainingType.values[json['type']],
durationWeeks: json['durationWeeks'],
sessionsPerWeek: json['sessionsPerWeek'],
targetMuscles: List<String>.from(json['targetMuscles']),
exercises: (json['exercises'] as List)
.map((e) => exerciseFromJson(e))
.toList(),
imageUrl: json['imageUrl'] ?? '',
createdDate: DateTime.parse(json['createdDate']),
isActive: json['isActive'] ?? true,
notes: json['notes'] ?? '',
estimatedCalories: json['estimatedCalories'] ?? 0,
estimatedDuration: json['estimatedDuration'] ?? 0,
);
}
static Map<String, dynamic> exerciseToJson(Exercise exercise) {
return {
'id': exercise.id,
'name': exercise.name,
'description': exercise.description,
'type': exercise.type.index,
'primaryMuscle': exercise.primaryMuscle.index,
'secondaryMuscles': exercise.secondaryMuscles.map((m) => m.index).toList(),
'equipment': exercise.equipment.index,
'difficulty': exercise.difficulty.index,
'sets': exercise.sets,
'reps': exercise.reps,
'restSeconds': exercise.restSeconds,
'weight': exercise.weight,
'duration': exercise.duration,
'instructions': exercise.instructions,
'tips': exercise.tips,
'videoUrl': exercise.videoUrl,
'imageUrl': exercise.imageUrl,
'caloriesPerRep': exercise.caloriesPerRep,
};
}
static Exercise exerciseFromJson(Map<String, dynamic> json) {
return Exercise(
id: json['id'],
name: json['name'],
description: json['description'],
type: ExerciseType.values[json['type']],
primaryMuscle: MuscleGroup.values[json['primaryMuscle']],
secondaryMuscles: (json['secondaryMuscles'] as List)
.map((m) => MuscleGroup.values[m])
.toList(),
equipment: EquipmentType.values[json['equipment']],
difficulty: DifficultyLevel.values[json['difficulty']],
sets: json['sets'],
reps: json['reps'],
restSeconds: json['restSeconds'],
weight: json['weight']?.toDouble() ?? 0.0,
duration: json['duration'],
instructions: json['instructions'] ?? '',
tips: List<String>.from(json['tips'] ?? []),
videoUrl: json['videoUrl'] ?? '',
imageUrl: json['imageUrl'] ?? '',
caloriesPerRep: json['caloriesPerRep'] ?? 1,
);
}
static Map<String, dynamic> recordToJson(TrainingRecord record) {
return {
'id': record.id,
'planId': record.planId,
'exerciseId': record.exerciseId,
'date': record.date.toIso8601String(),
'completedSets': record.completedSets,
'completedReps': record.completedReps,
'actualWeight': record.actualWeight,
'actualDuration': record.actualDuration,
'caloriesBurned': record.caloriesBurned,
'rating': record.rating,
'notes': record.notes,
'status': record.status.index,
'startTime': record.startTime.toIso8601String(),
'endTime': record.endTime?.toIso8601String(),
};
}
static TrainingRecord recordFromJson(Map<String, dynamic> json) {
return TrainingRecord(
id: json['id'],
planId: json['planId'],
exerciseId: json['exerciseId'],
date: DateTime.parse(json['date']),
completedSets: json['completedSets'] ?? 0,
completedReps: json['completedReps'] ?? 0,
actualWeight: json['actualWeight']?.toDouble() ?? 0.0,
actualDuration: json['actualDuration'] ?? 0,
caloriesBurned: json['caloriesBurned'] ?? 0,
rating: json['rating']?.toDouble() ?? 0.0,
notes: json['notes'] ?? '',
status: TrainingStatus.values[json['status']],
startTime: DateTime.parse(json['startTime']),
endTime: json['endTime'] != null ? DateTime.parse(json['endTime']) : null,
);
}
}
6. 健身计算工具
健身指标计算
class FitnessCalculator {
// 计算最大心率
static int calculateMaxHeartRate(int age) {
return 220 - age;
}
// 计算目标心率区间
static Map<String, int> calculateTargetHeartRate(int age) {
final maxHR = calculateMaxHeartRate(age);
return {
'fatBurn_min': (maxHR * 0.6).round(), // 燃脂区间下限
'fatBurn_max': (maxHR * 0.7).round(), // 燃脂区间上限
'cardio_min': (maxHR * 0.7).round(), // 有氧区间下限
'cardio_max': (maxHR * 0.85).round(), // 有氧区间上限
'anaerobic_min': (maxHR * 0.85).round(), // 无氧区间下限
'anaerobic_max': maxHR, // 无氧区间上限
};
}
// 计算一次性最大重量(1RM)
static double calculateOneRepMax(double weight, int reps) {
if (reps == 1) return weight;
// 使用Brzycki公式
return weight * (36 / (37 - reps));
}
// 计算训练强度百分比
static double calculateIntensityPercentage(double currentWeight, double oneRepMax) {
if (oneRepMax == 0) return 0.0;
return (currentWeight / oneRepMax) * 100;
}
// 计算休息时间建议
static int calculateRestTime(TrainingType type, DifficultyLevel difficulty) {
switch (type) {
case TrainingType.strength:
switch (difficulty) {
case DifficultyLevel.easy:
return 60;
case DifficultyLevel.medium:
return 90;
case DifficultyLevel.hard:
return 120;
case DifficultyLevel.extreme:
return 180;
}
case TrainingType.hiit:
return 30;
case TrainingType.cardio:
return 0; // 连续进行
default:
return 60;
}
}
// 计算训练量(Volume)
static int calculateTrainingVolume(List<Exercise> exercises) {
return exercises.fold(0, (sum, exercise) {
return sum + (exercise.sets * exercise.reps);
});
}
// 计算训练密度
static double calculateTrainingDensity(int totalVolume, int totalTimeMinutes) {
if (totalTimeMinutes == 0) return 0.0;
return totalVolume / totalTimeMinutes;
}
}
功能扩展建议
1. 智能训练推荐系统
AI训练计划生成
class AITrainingPlanner {
static TrainingPlan generatePersonalizedPlan({
required int age,
required double weight,
required double height,
required TrainingLevel level,
required List<String> goals,
required List<EquipmentType> availableEquipment,
required int availableDaysPerWeek,
required int sessionDurationMinutes,
}) {
// 基于用户数据生成个性化训练计划
final bmi = weight / (height * height);
final exercises = <Exercise>[];
// 根据目标选择练习
if (goals.contains('减脂')) {
exercises.addAll(_getCardioExercises(availableEquipment, level));
exercises.addAll(_getHIITExercises(availableEquipment, level));
}
if (goals.contains('增肌')) {
exercises.addAll(_getStrengthExercises(availableEquipment, level));
}
if (goals.contains('塑形')) {
exercises.addAll(_getFunctionalExercises(availableEquipment, level));
}
return TrainingPlan(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: 'AI定制计划',
description: '基于您的身体数据和目标定制的专属训练计划',
level: level,
type: _determineTrainingType(goals),
durationWeeks: _calculateOptimalDuration(level, goals),
sessionsPerWeek: availableDaysPerWeek,
targetMuscles: _getTargetMuscles(goals),
exercises: exercises,
createdDate: DateTime.now(),
estimatedCalories: _calculateEstimatedCalories(exercises, weight),
estimatedDuration: sessionDurationMinutes,
);
}
static List<Exercise> _getCardioExercises(List<EquipmentType> equipment, TrainingLevel level) {
// 根据器械和水平返回有氧练习
return [];
}
static TrainingType _determineTrainingType(List<String> goals) {
if (goals.length > 1) return TrainingType.mixed;
if (goals.contains('减脂')) return TrainingType.cardio;
if (goals.contains('增肌')) return TrainingType.strength;
return TrainingType.mixed;
}
}
智能进度调整
class ProgressiveOverload {
static TrainingPlan adjustPlanDifficulty(
TrainingPlan currentPlan,
List<TrainingRecord> recentRecords,
) {
final averageRating = recentRecords.fold(0.0, (sum, r) => sum + r.rating) / recentRecords.length;
final completionRate = recentRecords.where((r) => r.status == TrainingStatus.completed).length / recentRecords.length;
if (averageRating >= 4.0 && completionRate >= 0.8) {
// 增加难度
return _increaseDifficulty(currentPlan);
} else if (averageRating <= 2.0 || completionRate <= 0.5) {
// 降低难度
return _decreaseDifficulty(currentPlan);
}
return currentPlan;
}
static TrainingPlan _increaseDifficulty(TrainingPlan plan) {
final adjustedExercises = plan.exercises.map((exercise) {
if (exercise.type == ExerciseType.strength) {
return exercise.copyWith(
weight: exercise.weight * 1.05, // 增加5%重量
reps: exercise.reps + 1, // 增加1次
);
} else if (exercise.type == ExerciseType.cardio) {
return exercise.copyWith(
duration: (exercise.duration * 1.1).round(), // 增加10%时长
);
}
return exercise;
}).toList();
return plan.copyWith(exercises: adjustedExercises);
}
}
2. 社交健身功能
好友系统
class FitnessUser {
final String id;
final String name;
final String avatar;
final int level;
final List<String> achievements;
final FitnessStats stats;
const FitnessUser({
required this.id,
required this.name,
required this.avatar,
required this.level,
required this.achievements,
required this.stats,
});
}
class FitnessStats {
final int totalWorkouts;
final int totalCalories;
final int currentStreak;
final int longestStreak;
final double averageRating;
const FitnessStats({
required this.totalWorkouts,
required this.totalCalories,
required this.currentStreak,
required this.longestStreak,
required this.averageRating,
});
}
class SocialFitnessService {
static Future<List<FitnessUser>> getFriends(String userId) async {
// 获取好友列表
return [];
}
static Future<void> shareWorkout(TrainingRecord record) async {
// 分享训练记录到社交平台
}
static Future<List<Challenge>> getActiveChallenges() async {
// 获取活跃的健身挑战
return [];
}
}
挑战系统
class Challenge {
final String id;
final String name;
final String description;
final ChallengeType type;
final DateTime startDate;
final DateTime endDate;
final Map<String, dynamic> target;
final List<String> participants;
final String reward;
const Challenge({
required this.id,
required this.name,
required this.description,
required this.type,
required this.startDate,
required this.endDate,
required this.target,
required this.participants,
required this.reward,
});
}
enum ChallengeType {
dailyWorkout, // 每日训练
weeklyCalories, // 周卡路里
monthlyStreak, // 月连续
specificExercise, // 特定练习
}
class ChallengeManager {
static List<Challenge> generateWeeklyChallenges() {
return [
Challenge(
id: '1',
name: '7天训练挑战',
description: '连续7天完成训练',
type: ChallengeType.dailyWorkout,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 7)),
target: {'days': 7},
participants: [],
reward: '健身达人徽章',
),
Challenge(
id: '2',
name: '周燃脂挑战',
description: '本周消耗2000卡路里',
type: ChallengeType.weeklyCalories,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 7)),
target: {'calories': 2000},
participants: [],
reward: '燃脂王者称号',
),
];
}
}
3. 健康数据集成
健康数据同步
// 添加依赖:health
class HealthDataIntegration {
static Future<bool> requestPermissions() async {
final health = Health();
final types = [
HealthDataType.STEPS,
HealthDataType.HEART_RATE,
HealthDataType.WEIGHT,
HealthDataType.ACTIVE_ENERGY_BURNED,
];
return await health.requestAuthorization(types);
}
static Future<Map<String, dynamic>> getTodayHealthData() async {
final health = Health();
final now = DateTime.now();
final startOfDay = DateTime(now.year, now.month, now.day);
final steps = await health.getTotalStepsInInterval(startOfDay, now);
final heartRate = await health.getHealthDataFromTypes(
[HealthDataType.HEART_RATE],
startOfDay,
now,
);
return {
'steps': steps ?? 0,
'heartRate': heartRate.isNotEmpty ? heartRate.last.value : 0,
'date': now,
};
}
static Future<void> writeWorkoutData(TrainingRecord record) async {
final health = Health();
await health.writeHealthData(
record.caloriesBurned.toDouble(),
HealthDataType.ACTIVE_ENERGY_BURNED,
record.startTime,
record.endTime ?? DateTime.now(),
);
}
}
可穿戴设备集成
class WearableDeviceManager {
static Future<bool> connectToDevice(String deviceType) async {
switch (deviceType) {
case 'apple_watch':
return await _connectToAppleWatch();
case 'fitbit':
return await _connectToFitbit();
case 'garmin':
return await _connectToGarmin();
default:
return false;
}
}
static Future<Map<String, dynamic>> getRealtimeData() async {
// 获取实时心率、步数等数据
return {
'heartRate': 75,
'steps': 8500,
'calories': 320,
'timestamp': DateTime.now(),
};
}
static Future<bool> _connectToAppleWatch() async {
// Apple Watch连接逻辑
return true;
}
}
4. 营养管理集成
营养计划
class NutritionPlan {
final String id;
final String name;
final int dailyCalories;
final Map<String, double> macroRatios; // 蛋白质、碳水、脂肪比例
final List<MealPlan> meals;
final DateTime createdDate;
const NutritionPlan({
required this.id,
required this.name,
required this.dailyCalories,
required this.macroRatios,
required this.meals,
required this.createdDate,
});
}
class MealPlan {
final String name;
final int calories;
final Map<String, double> nutrients;
final List<String> foods;
final String mealTime;
const MealPlan({
required this.name,
required this.calories,
required this.nutrients,
required this.foods,
required this.mealTime,
});
}
class NutritionCalculator {
static NutritionPlan generateNutritionPlan({
required double weight,
required double height,
required int age,
required String gender,
required String goal,
required TrainingLevel activityLevel,
}) {
// 计算基础代谢率(BMR)
final bmr = _calculateBMR(weight, height, age, gender);
// 计算总日消耗(TDEE)
final tdee = _calculateTDEE(bmr, activityLevel);
// 根据目标调整卡路里
final targetCalories = _adjustCaloriesForGoal(tdee, goal);
// 计算宏量营养素比例
final macroRatios = _calculateMacroRatios(goal);
return NutritionPlan(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: '个性化营养计划',
dailyCalories: targetCalories.round(),
macroRatios: macroRatios,
meals: _generateMealPlans(targetCalories, macroRatios),
createdDate: DateTime.now(),
);
}
static double _calculateBMR(double weight, double height, int age, String gender) {
// 使用Mifflin-St Jeor公式
if (gender.toLowerCase() == 'male') {
return 10 * weight + 6.25 * height - 5 * age + 5;
} else {
return 10 * weight + 6.25 * height - 5 * age - 161;
}
}
static double _calculateTDEE(double bmr, TrainingLevel activityLevel) {
switch (activityLevel) {
case TrainingLevel.beginner:
return bmr * 1.2; // 久坐
case TrainingLevel.intermediate:
return bmr * 1.55; // 中等活动
case TrainingLevel.advanced:
return bmr * 1.725; // 高活动
case TrainingLevel.expert:
return bmr * 1.9; // 极高活动
}
}
}
5. 虚拟教练功能
AI语音指导
// 添加依赖:flutter_tts, speech_to_text
class VirtualCoach {
static final FlutterTts _tts = FlutterTts();
static final SpeechToText _speech = SpeechToText();
static Future<void> initializeCoach() async {
await _tts.setLanguage('zh-CN');
await _tts.setSpeechRate(0.8);
await _tts.setVolume(0.8);
await _tts.setPitch(1.0);
}
static Future<void> provideExerciseGuidance(Exercise exercise) async {
final guidance = _generateGuidanceText(exercise);
await _tts.speak(guidance);
}
static Future<void> countdownRest(int seconds) async {
for (int i = seconds; i > 0; i--) {
if (i <= 10) {
await _tts.speak('$i');
await Future.delayed(const Duration(seconds: 1));
} else {
await Future.delayed(const Duration(seconds: 1));
}
}
await _tts.speak('休息结束,开始下一组');
}
static Future<void> motivate(String message) async {
final motivationalMessages = [
'你做得很棒!继续保持!',
'坚持就是胜利!',
'每一次训练都让你更强!',
'相信自己,你可以的!',
'今天的汗水是明天的成果!',
];
final randomMessage = motivationalMessages[
DateTime.now().millisecond % motivationalMessages.length
];
await _tts.speak(message.isEmpty ? randomMessage : message);
}
static String _generateGuidanceText(Exercise exercise) {
return '现在开始${exercise.name},'
'完成${exercise.sets}组,每组${exercise.reps}次。'
'${exercise.instructions}';
}
static Future<String?> listenToCommand() async {
final available = await _speech.initialize();
if (!available) return null;
final completer = Completer<String?>();
_speech.listen(
onResult: (result) {
if (result.finalResult) {
completer.complete(result.recognizedWords);
}
},
localeId: 'zh_CN',
);
return completer.future;
}
}
动作识别与纠正
// 添加依赖:camera, tflite
class PoseDetection {
static Future<bool> initializeModel() async {
try {
await Tflite.loadModel(
model: 'assets/models/pose_detection.tflite',
labels: 'assets/models/pose_labels.txt',
);
return true;
} catch (e) {
return false;
}
}
static Future<List<PoseKeypoint>> detectPose(String imagePath) async {
final recognitions = await Tflite.runPoseNetOnImage(
path: imagePath,
numResults: 1,
);
return recognitions?.map((r) => PoseKeypoint.fromMap(r)).toList() ?? [];
}
static ExerciseForm analyzePushupForm(List<PoseKeypoint> keypoints) {
// 分析俯卧撑动作标准性
final leftShoulder = keypoints.firstWhere((k) => k.part == 'leftShoulder');
final rightShoulder = keypoints.firstWhere((k) => k.part == 'rightShoulder');
final leftElbow = keypoints.firstWhere((k) => k.part == 'leftElbow');
final rightElbow = keypoints.firstWhere((k) => k.part == 'rightElbow');
// 检查肩膀是否水平
final shoulderLevel = (leftShoulder.y - rightShoulder.y).abs();
final isShoulderLevel = shoulderLevel < 0.05;
// 检查手肘角度
final elbowAngle = _calculateAngle(leftShoulder, leftElbow, keypoints.firstWhere((k) => k.part == 'leftWrist'));
final isElbowAngleCorrect = elbowAngle >= 45 && elbowAngle <= 90;
return ExerciseForm(
isCorrect: isShoulderLevel && isElbowAngleCorrect,
feedback: _generateFormFeedback(isShoulderLevel, isElbowAngleCorrect),
score: _calculateFormScore(isShoulderLevel, isElbowAngleCorrect),
);
}
static double _calculateAngle(PoseKeypoint point1, PoseKeypoint point2, PoseKeypoint point3) {
// 计算三点之间的角度
final vector1 = [point1.x - point2.x, point1.y - point2.y];
final vector2 = [point3.x - point2.x, point3.y - point2.y];
final dotProduct = vector1[0] * vector2[0] + vector1[1] * vector2[1];
final magnitude1 = sqrt(vector1[0] * vector1[0] + vector1[1] * vector1[1]);
final magnitude2 = sqrt(vector2[0] * vector2[0] + vector2[1] * vector2[1]);
final cosAngle = dotProduct / (magnitude1 * magnitude2);
return acos(cosAngle) * 180 / pi;
}
}
class PoseKeypoint {
final String part;
final double x;
final double y;
final double confidence;
const PoseKeypoint({
required this.part,
required this.x,
required this.y,
required this.confidence,
});
factory PoseKeypoint.fromMap(Map<String, dynamic> map) {
return PoseKeypoint(
part: map['part'],
x: map['x'],
y: map['y'],
confidence: map['confidence'],
);
}
}
class ExerciseForm {
final bool isCorrect;
final String feedback;
final double score;
const ExerciseForm({
required this.isCorrect,
required this.feedback,
required this.score,
});
}
6. 数据分析与可视化
高级图表展示
// 添加依赖:fl_chart
class FitnessCharts {
static Widget buildProgressChart(List<TrainingRecord> records) {
return LineChart(
LineChartData(
gridData: FlGridData(show: true),
titlesData: FlTitlesData(show: true),
borderData: FlBorderData(show: true),
lineBarsData: [
LineChartBarData(
spots: _generateProgressSpots(records),
isCurved: true,
color: Colors.orange,
barWidth: 3,
dotData: FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: Colors.orange.withValues(alpha: 0.3),
),
),
],
),
);
}
static Widget buildCalorieChart(List<TrainingRecord> records) {
return BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: _getMaxCalories(records),
barTouchData: BarTouchData(enabled: true),
titlesData: FlTitlesData(show: true),
borderData: FlBorderData(show: false),
barGroups: _generateCalorieBarGroups(records),
),
);
}
static Widget buildMuscleGroupChart(List<TrainingRecord> records) {
return PieChart(
PieChartData(
sections: _generateMuscleGroupSections(records),
centerSpaceRadius: 40,
sectionsSpace: 2,
),
);
}
static List<FlSpot> _generateProgressSpots(List<TrainingRecord> records) {
final weeklyData = <int, double>{};
for (int i = 0; i < 12; i++) {
final weekStart = DateTime.now().subtract(Duration(days: (11 - i) * 7));
final weekEnd = weekStart.add(const Duration(days: 7));
final weekRecords = records.where((r) =>
r.date.isAfter(weekStart) && r.date.isBefore(weekEnd) &&
r.status == TrainingStatus.completed).toList();
final totalCalories = weekRecords.fold(0, (sum, r) => sum + r.caloriesBurned);
weeklyData[i] = totalCalories.toDouble();
}
return weeklyData.entries.map((e) => FlSpot(e.key.toDouble(), e.value)).toList();
}
}
r2[0] * vector2[0] + vector2[1] * vector2[1]);
final cosAngle = dotProduct / (magnitude1 * magnitude2);
return acos(cosAngle) * 180 / pi;
}
static String _generateFormFeedback(bool isShoulderLevel, bool isElbowAngleCorrect) {
final feedback = [];
if (!isShoulderLevel) {
feedback.add('保持肩膀水平');
}
if (!isElbowAngleCorrect) {
feedback.add('调整手肘角度至45-90度');
}
if (feedback.isEmpty) {
return '动作标准,继续保持!';
}
return '建议:${feedback.join(',')}';
}
static double _calculateFormScore(bool isShoulderLevel, bool isElbowAngleCorrect) {
double score = 0.0;
if (isShoulderLevel) score += 50.0;
if (isElbowAngleCorrect) score += 50.0;
return score;
}
}
class PoseKeypoint {
final String part;
final double x;
final double y;
final double confidence;
const PoseKeypoint({
required this.part,
required this.x,
required this.y,
required this.confidence,
});
factory PoseKeypoint.fromMap(Map<String, dynamic> map) {
return PoseKeypoint(
part: map[‘part’],
x: map[‘x’]?.toDouble() ?? 0.0,
y: map[‘y’]?.toDouble() ?? 0.0,
confidence: map[‘confidence’]?.toDouble() ?? 0.0,
);
}
}
class ExerciseForm {
final bool isCorrect;
final String feedback;
final double score;
const ExerciseForm({
required this.isCorrect,
required this.feedback,
required this.score,
});
}
### 6. 数据可视化增强
#### 训练进度图表
```dart
// 添加依赖:fl_chart
class ProgressCharts {
static Widget buildCaloriesChart(List<TrainingRecord> records) {
final data = _prepareCaloriesData(records);
return LineChart(
LineChartData(
gridData: FlGridData(show: true),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) => Text('${value.toInt()}'),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) => Text(_formatChartDate(value)),
),
),
),
borderData: FlBorderData(show: true),
lineBarsData: [
LineChartBarData(
spots: data,
isCurved: true,
color: Colors.orange,
barWidth: 3,
dotData: FlDotData(show: true),
),
],
),
);
}
static Widget buildWorkoutFrequencyChart(List<TrainingRecord> records) {
final data = _prepareFrequencyData(records);
return BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: data.map((e) => e.y).reduce((a, b) => a > b ? a : b) * 1.2,
barTouchData: BarTouchData(enabled: true),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) => Text('${value.toInt()}'),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) => Text(_getWeekdayName(value.toInt())),
),
),
),
borderData: FlBorderData(show: false),
barGroups: data.asMap().entries.map((entry) {
return BarChartGroupData(
x: entry.key,
barRods: [
BarChartRodData(
toY: entry.value.y,
color: Colors.orange,
width: 20,
borderRadius: BorderRadius.circular(4),
),
],
);
}).toList(),
),
);
}
static Widget buildMuscleGroupDistribution(List<TrainingPlan> plans) {
final data = _prepareMuscleGroupData(plans);
return PieChart(
PieChartData(
sections: data.asMap().entries.map((entry) {
final index = entry.key;
final data = entry.value;
return PieChartSectionData(
color: _getMuscleGroupColor(data.muscleGroup),
value: data.percentage,
title: '${data.percentage.toStringAsFixed(1)}%',
radius: 100,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
);
}).toList(),
sectionsSpace: 2,
centerSpaceRadius: 40,
),
);
}
static List<FlSpot> _prepareCaloriesData(List<TrainingRecord> records) {
final dailyCalories = <DateTime, int>{};
for (final record in records) {
final date = DateTime(record.date.year, record.date.month, record.date.day);
dailyCalories[date] = (dailyCalories[date] ?? 0) + record.caloriesBurned;
}
final sortedEntries = dailyCalories.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
return sortedEntries.asMap().entries.map((entry) {
return FlSpot(entry.key.toDouble(), entry.value.value.toDouble());
}).toList();
}
static List<FlSpot> _prepareFrequencyData(List<TrainingRecord> records) {
final weekdayCount = List.filled(7, 0);
for (final record in records) {
if (record.status == TrainingStatus.completed) {
weekdayCount[record.date.weekday - 1]++;
}
}
return weekdayCount.asMap().entries.map((entry) {
return FlSpot(entry.key.toDouble(), entry.value.toDouble());
}).toList();
}
static List<MuscleGroupData> _prepareMuscleGroupData(List<TrainingPlan> plans) {
final muscleGroupCount = <MuscleGroup, int>{};
for (final plan in plans) {
for (final exercise in plan.exercises) {
muscleGroupCount[exercise.primaryMuscle] =
(muscleGroupCount[exercise.primaryMuscle] ?? 0) + 1;
}
}
final total = muscleGroupCount.values.fold(0, (sum, count) => sum + count);
return muscleGroupCount.entries.map((entry) {
return MuscleGroupData(
muscleGroup: entry.key,
count: entry.value,
percentage: (entry.value / total) * 100,
);
}).toList();
}
static String _formatChartDate(double value) {
final date = DateTime.now().subtract(Duration(days: (30 - value).toInt()));
return '${date.month}/${date.day}';
}
static String _getWeekdayName(int weekday) {
const weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
return weekdays[weekday];
}
}
class MuscleGroupData {
final MuscleGroup muscleGroup;
final int count;
final double percentage;
const MuscleGroupData({
required this.muscleGroup,
required this.count,
required this.percentage,
});
}
性能优化策略
1. 内存管理优化
图片缓存优化
class ImageCacheManager {
static final Map<String, Uint8List> _cache = {};
static const int _maxCacheSize = 50; // 最大缓存50张图片
static Future<ImageProvider> getImage(String url) async {
if (_cache.containsKey(url)) {
return MemoryImage(_cache[url]!);
}
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final bytes = response.bodyBytes;
// 如果缓存已满,移除最旧的图片
if (_cache.length >= _maxCacheSize) {
final firstKey = _cache.keys.first;
_cache.remove(firstKey);
}
_cache[url] = bytes;
return MemoryImage(bytes);
}
} catch (e) {
// 返回默认图片
return const AssetImage('assets/images/default_exercise.png');
}
return const AssetImage('assets/images/default_exercise.png');
}
static void clearCache() {
_cache.clear();
}
static int getCacheSize() {
return _cache.length;
}
}
列表性能优化
class OptimizedTrainingPlanList extends StatefulWidget {
final List<TrainingPlan> plans;
final Function(TrainingPlan) onPlanTap;
const OptimizedTrainingPlanList({
super.key,
required this.plans,
required this.onPlanTap,
});
State<OptimizedTrainingPlanList> createState() => _OptimizedTrainingPlanListState();
}
class _OptimizedTrainingPlanListState extends State<OptimizedTrainingPlanList> {
final ScrollController _scrollController = ScrollController();
void dispose() {
_scrollController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
itemCount: widget.plans.length,
// 使用 itemExtent 提高性能
itemExtent: 120.0,
// 缓存范围优化
cacheExtent: 500.0,
itemBuilder: (context, index) {
final plan = widget.plans[index];
return RepaintBoundary(
child: TrainingPlanCard(
plan: plan,
onTap: () => widget.onPlanTap(plan),
),
);
},
);
}
}
class TrainingPlanCard extends StatelessWidget {
final TrainingPlan plan;
final VoidCallback onTap;
const TrainingPlanCard({
super.key,
required this.plan,
required this.onTap,
});
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// 使用 Hero 动画优化页面切换
Hero(
tag: 'plan_${plan.id}',
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: _getTrainingTypeColor(plan.type).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getTrainingTypeIcon(plan.type),
size: 30,
color: _getTrainingTypeColor(plan.type),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
plan.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'${plan.estimatedDuration}分钟 • ${plan.estimatedCalories}卡路里',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
Text(
_getTrainingLevelName(plan.level),
style: TextStyle(
fontSize: 12,
color: _getTrainingTypeColor(plan.type),
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
),
);
}
}
2. 数据库性能优化
批量操作优化
class DatabaseOptimizer {
static Future<void> batchInsertRecords(List<TrainingRecord> records) async {
final db = await DatabaseHelper.database;
await db.transaction((txn) async {
final batch = txn.batch();
for (final record in records) {
batch.insert(
'training_records',
DataSerializer.recordToJson(record),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
});
}
static Future<List<TrainingRecord>> getRecordsWithPagination({
required int offset,
required int limit,
String? planId,
}) async {
final db = await DatabaseHelper.database;
String whereClause = '';
List<dynamic> whereArgs = [];
if (planId != null) {
whereClause = 'WHERE plan_id = ?';
whereArgs.add(planId);
}
final List<Map<String, dynamic>> maps = await db.rawQuery('''
SELECT * FROM training_records
$whereClause
ORDER BY date DESC
LIMIT ? OFFSET ?
''', [...whereArgs, limit, offset]);
return maps.map((map) => DataSerializer.recordFromJson(map)).toList();
}
static Future<void> createIndexes() async {
final db = await DatabaseHelper.database;
// 为常用查询字段创建索引
await db.execute('CREATE INDEX IF NOT EXISTS idx_records_date ON training_records(date)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_records_plan_id ON training_records(plan_id)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_records_status ON training_records(status)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_plans_active ON training_plans(is_active)');
await db.execute('CREATE INDEX IF NOT EXISTS idx_plans_type ON training_plans(type)');
}
static Future<void> optimizeDatabase() async {
final db = await DatabaseHelper.database;
// 分析表统计信息
await db.execute('ANALYZE');
// 清理碎片
await db.execute('VACUUM');
}
}
3. 网络请求优化
请求缓存和重试机制
class NetworkOptimizer {
static final Map<String, CachedResponse> _cache = {};
static const Duration _cacheExpiry = Duration(minutes: 5);
static Future<http.Response> optimizedGet(
String url, {
Duration? cacheExpiry,
int maxRetries = 3,
}) async {
final cacheKey = url;
final cached = _cache[cacheKey];
// 检查缓存
if (cached != null &&
DateTime.now().difference(cached.timestamp) < (cacheExpiry ?? _cacheExpiry)) {
return http.Response(cached.body, cached.statusCode);
}
// 执行请求,带重试机制
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
final response = await http.get(
Uri.parse(url),
headers: {
'User-Agent': 'FitnessTrainingApp/1.0',
'Accept': 'application/json',
},
).timeout(const Duration(seconds: 10));
// 缓存成功响应
if (response.statusCode == 200) {
_cache[cacheKey] = CachedResponse(
body: response.body,
statusCode: response.statusCode,
timestamp: DateTime.now(),
);
}
return response;
} catch (e) {
if (attempt == maxRetries - 1) {
rethrow;
}
// 指数退避
await Future.delayed(Duration(milliseconds: 1000 * (attempt + 1)));
}
}
throw Exception('Max retries exceeded');
}
static void clearCache() {
_cache.clear();
}
static void removeExpiredCache() {
final now = DateTime.now();
_cache.removeWhere((key, value) =>
now.difference(value.timestamp) > _cacheExpiry);
}
}
class CachedResponse {
final String body;
final int statusCode;
final DateTime timestamp;
const CachedResponse({
required this.body,
required this.statusCode,
required this.timestamp,
});
}
4. UI渲染优化
动画性能优化
class OptimizedAnimations {
static Widget buildFadeInAnimation({
required Widget child,
Duration duration = const Duration(milliseconds: 300),
Curve curve = Curves.easeInOut,
}) {
return TweenAnimationBuilder<double>(
duration: duration,
tween: Tween<double>(begin: 0.0, end: 1.0),
curve: curve,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: child,
),
);
},
child: child,
);
}
static Widget buildSlideAnimation({
required Widget child,
required AnimationController controller,
Offset begin = const Offset(1.0, 0.0),
Offset end = Offset.zero,
}) {
final animation = Tween<Offset>(
begin: begin,
end: end,
).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOutCubic,
));
return SlideTransition(
position: animation,
child: child,
);
}
static Widget buildScaleAnimation({
required Widget child,
required AnimationController controller,
double begin = 0.0,
double end = 1.0,
}) {
final animation = Tween<double>(
begin: begin,
end: end,
).animate(CurvedAnimation(
parent: controller,
curve: Curves.elasticOut,
));
return ScaleTransition(
scale: animation,
child: child,
);
}
}
测试指南
1. 单元测试
数据模型测试
// test/models/training_plan_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:fitness_training/models/training_plan.dart';
void main() {
group('TrainingPlan Tests', () {
late TrainingPlan testPlan;
setUp(() {
testPlan = TrainingPlan(
id: 'test_1',
name: '测试计划',
description: '这是一个测试计划',
level: TrainingLevel.beginner,
type: TrainingType.strength,
durationWeeks: 4,
sessionsPerWeek: 3,
targetMuscles: ['胸部', '手臂'],
exercises: [],
createdDate: DateTime.now(),
estimatedCalories: 200,
estimatedDuration: 30,
);
});
test('should create TrainingPlan with correct properties', () {
expect(testPlan.id, 'test_1');
expect(testPlan.name, '测试计划');
expect(testPlan.level, TrainingLevel.beginner);
expect(testPlan.type, TrainingType.strength);
expect(testPlan.isActive, true);
});
test('should copy TrainingPlan with new properties', () {
final copiedPlan = testPlan.copyWith(
name: '新计划名称',
level: TrainingLevel.intermediate,
);
expect(copiedPlan.name, '新计划名称');
expect(copiedPlan.level, TrainingLevel.intermediate);
expect(copiedPlan.id, testPlan.id); // 未改变的属性应保持不变
});
test('should validate plan data completeness', () {
expect(_isPlanDataComplete(testPlan), true);
final incompletePlan = testPlan.copyWith(name: '');
expect(_isPlanDataComplete(incompletePlan), false);
});
});
}
工具方法测试
// test/utils/fitness_calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:fitness_training/utils/fitness_calculator.dart';
void main() {
group('FitnessCalculator Tests', () {
test('should calculate max heart rate correctly', () {
expect(FitnessCalculator.calculateMaxHeartRate(25), 195);
expect(FitnessCalculator.calculateMaxHeartRate(40), 180);
expect(FitnessCalculator.calculateMaxHeartRate(60), 160);
});
test('should calculate target heart rate zones', () {
final zones = FitnessCalculator.calculateTargetHeartRate(30);
expect(zones['fatBurn_min'], 114); // (220-30) * 0.6
expect(zones['fatBurn_max'], 133); // (220-30) * 0.7
expect(zones['cardio_min'], 133); // (220-30) * 0.7
expect(zones['cardio_max'], 162); // (220-30) * 0.85
});
test('should calculate one rep max correctly', () {
expect(FitnessCalculator.calculateOneRepMax(100, 1), 100);
expect(FitnessCalculator.calculateOneRepMax(80, 5), closeTo(90.0, 1.0));
expect(FitnessCalculator.calculateOneRepMax(60, 10), closeTo(80.0, 2.0));
});
test('should calculate BMI correctly', () {
expect(FitnessStatistics.calculateBMI(70, 1.75), closeTo(22.86, 0.01));
expect(FitnessStatistics.calculateBMI(80, 1.80), closeTo(24.69, 0.01));
expect(FitnessStatistics.calculateBMI(0, 1.75), 0.0);
});
});
}
2. Widget测试
训练计划卡片测试
// test/widgets/training_plan_card_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fitness_training/widgets/training_plan_card.dart';
import 'package:fitness_training/models/training_plan.dart';
void main() {
group('TrainingPlanCard Widget Tests', () {
late TrainingPlan testPlan;
setUp(() {
testPlan = TrainingPlan(
id: 'test_1',
name: '测试计划',
description: '测试描述',
level: TrainingLevel.beginner,
type: TrainingType.strength,
durationWeeks: 4,
sessionsPerWeek: 3,
targetMuscles: ['胸部'],
exercises: [],
createdDate: DateTime.now(),
estimatedCalories: 200,
estimatedDuration: 30,
);
});
testWidgets('should display plan information correctly', (tester) async {
bool tapped = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TrainingPlanCard(
plan: testPlan,
onTap: () => tapped = true,
),
),
),
);
// 验证计划名称显示
expect(find.text('测试计划'), findsOneWidget);
// 验证时长和卡路里显示
expect(find.text('30分钟 • 200卡路里'), findsOneWidget);
// 验证难度等级显示
expect(find.text('初级'), findsOneWidget);
// 测试点击事件
await tester.tap(find.byType(TrainingPlanCard));
await tester.pump();
expect(tapped, true);
});
testWidgets('should show correct training type icon', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TrainingPlanCard(
plan: testPlan,
onTap: () {},
),
),
),
);
// 验证力量训练图标
expect(find.byIcon(Icons.fitness_center), findsOneWidget);
});
});
}
3. 集成测试
完整训练流程测试
// integration_test/training_flow_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:fitness_training/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Training Flow Integration Tests', () {
testWidgets('complete training session flow', (tester) async {
app.main();
await tester.pumpAndSettle();
// 1. 验证应用启动
expect(find.text('今日训练'), findsOneWidget);
// 2. 导航到训练计划页面
await tester.tap(find.text('训练计划'));
await tester.pumpAndSettle();
// 3. 创建新的训练计划
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
// 填写计划信息
await tester.enterText(find.byKey(const Key('plan_name_field')), '集成测试计划');
await tester.enterText(find.byKey(const Key('plan_description_field')), '这是集成测试创建的计划');
// 保存计划
await tester.tap(find.text('保存'));
await tester.pumpAndSettle();
// 4. 验证计划创建成功
expect(find.text('集成测试计划'), findsOneWidget);
// 5. 开始训练
await tester.tap(find.text('开始训练'));
await tester.pumpAndSettle();
// 6. 完成训练
await tester.tap(find.text('完成练习'));
await tester.pumpAndSettle();
// 7. 验证训练记录
await tester.tap(find.text('训练进度'));
await tester.pumpAndSettle();
expect(find.text('已完成'), findsOneWidget);
});
testWidgets('search and filter functionality', (tester) async {
app.main();
await tester.pumpAndSettle();
// 导航到训练计划页面
await tester.tap(find.text('训练计划'));
await tester.pumpAndSettle();
// 测试搜索功能
await tester.enterText(find.byKey(const Key('search_field')), '新手');
await tester.pumpAndSettle();
// 验证搜索结果
expect(find.text('新手入门计划'), findsOneWidget);
// 测试筛选功能
await tester.tap(find.text('类型'));
await tester.pumpAndSettle();
await tester.tap(find.text('力量训练'));
await tester.tap(find.text('确定'));
await tester.pumpAndSettle();
// 验证筛选结果
expect(find.text('力量提升计划'), findsOneWidget);
});
});
}
4. 性能测试
内存和渲染性能测试
// test/performance/performance_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fitness_training/pages/training_plans_page.dart';
import 'package:fitness_training/models/training_plan.dart';
void main() {
group('Performance Tests', () {
testWidgets('large list rendering performance', (tester) async {
// 创建大量测试数据
final plans = List.generate(1000, (index) => TrainingPlan(
id: 'plan_$index',
name: '计划 $index',
description: '描述 $index',
level: TrainingLevel.values[index % TrainingLevel.values.length],
type: TrainingType.values[index % TrainingType.values.length],
durationWeeks: 4,
sessionsPerWeek: 3,
targetMuscles: ['肌群$index'],
exercises: [],
createdDate: DateTime.now(),
estimatedCalories: 200 + index,
estimatedDuration: 30 + index % 60,
));
// 测量渲染时间
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(
MaterialApp(
home: TrainingPlansPage(plans: plans),
),
);
await tester.pumpAndSettle();
stopwatch.stop();
// 验证渲染时间在合理范围内(< 1秒)
expect(stopwatch.elapsedMilliseconds, lessThan(1000));
// 验证列表可以正常滚动
await tester.fling(find.byType(ListView), const Offset(0, -500), 1000);
await tester.pumpAndSettle();
// 验证内存使用情况
final binding = TestWidgetsFlutterBinding.ensureInitialized();
final memoryUsage = binding.defaultBinaryMessenger;
// 这里可以添加更详细的内存监控逻辑
});
testWidgets('animation performance test', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OptimizedAnimations.buildFadeInAnimation(
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
),
),
),
);
// 测量动画帧率
final frames = <Duration>[];
await tester.binding.addTime(const Duration(milliseconds: 16));
frames.add(const Duration(milliseconds: 16));
for (int i = 0; i < 60; i++) {
await tester.pump(const Duration(milliseconds: 16));
frames.add(Duration(milliseconds: 16 * (i + 2)));
}
// 验证动画流畅度(60fps)
expect(frames.length, 61);
});
});
}
部署指南
1. Android部署
构建配置
# android/app/build.gradle
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.fitness_training"
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "1.0.0"
multiDexEnabled true
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
权限配置
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 存储权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 健康数据权限 -->
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.BODY_SENSORS" />
<!-- 相机权限(用于动作识别) -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 音频权限(用于语音指导) -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 振动权限 -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- 通知权限 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:label="健身训练计划"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:theme="@style/LaunchTheme"
android:exported="true"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- 后台服务配置 -->
<service
android:name=".TrainingReminderService"
android:enabled="true"
android:exported="false" />
<!-- 广播接收器 -->
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
构建脚本
#!/bin/bash
# scripts/build_android.sh
echo "开始构建Android应用..."
# 清理之前的构建
flutter clean
# 获取依赖
flutter pub get
# 运行代码生成
flutter packages pub run build_runner build --delete-conflicting-outputs
# 运行测试
flutter test
# 构建APK
flutter build apk --release --split-per-abi
# 构建AAB(用于Google Play)
flutter build appbundle --release
echo "Android构建完成!"
echo "APK文件位置: build/app/outputs/flutter-apk/"
echo "AAB文件位置: build/app/outputs/bundle/release/"
2. iOS部署
项目配置
<!-- ios/Runner/Info.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>健身训练计划</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>fitness_training</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<!-- 权限描述 -->
<key>NSCameraUsageDescription</key>
<string>应用需要访问相机来进行动作识别和训练指导</string>
<key>NSMicrophoneUsageDescription</key>
<string>应用需要访问麦克风来提供语音指导功能</string>
<key>NSMotionUsageDescription</key>
<string>应用需要访问运动数据来追踪您的健身活动</string>
<key>NSHealthShareUsageDescription</key>
<string>应用需要读取健康数据来提供个性化的训练建议</string>
<key>NSHealthUpdateUsageDescription</key>
<string>应用需要写入健康数据来记录您的训练成果</string>
<!-- 后台模式 -->
<key>UIBackgroundModes</key>
<array>
<string>background-processing</string>
<string>background-fetch</string>
</array>
</dict>
</plist>
构建脚本
#!/bin/bash
# scripts/build_ios.sh
echo "开始构建iOS应用..."
# 清理之前的构建
flutter clean
# 获取依赖
flutter pub get
# 运行代码生成
flutter packages pub run build_runner build --delete-conflicting-outputs
# 运行测试
flutter test
# 构建iOS应用
flutter build ios --release --no-codesign
echo "iOS构建完成!"
echo "请在Xcode中打开项目进行签名和发布"
3. Web部署
构建配置
<!-- web/index.html -->
<!DOCTYPE html>
<html>
<head>
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="专业的健身训练计划管理应用">
<meta name="keywords" content="健身,训练,计划,运动,健康">
<meta name="author" content="Fitness Training Team">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="健身训练计划">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>健身训练计划</title>
<link rel="manifest" href="manifest.json">
<style>
body {
margin: 0;
padding: 0;
background-color: #f5f5f5;
font-family: 'Roboto', sans-serif;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #ff9800;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div id="loading" class="loading">
<div class="loading-spinner"></div>
<p>正在加载健身训练计划...</p>
</div>
<script>
window.addEventListener('flutter-first-frame', function () {
document.getElementById('loading').style.display = 'none';
});
</script>
<script src="flutter.js" defer></script>
</body>
</html>
PWA配置
// web/manifest.json
{
"name": "健身训练计划",
"short_name": "健身计划",
"description": "专业的健身训练计划管理应用",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ff9800",
"orientation": "portrait-primary",
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"categories": ["health", "fitness", "lifestyle"],
"lang": "zh-CN",
"scope": "/",
"screenshots": [
{
"src": "screenshots/home.png",
"sizes": "1080x1920",
"type": "image/png"
},
{
"src": "screenshots/plans.png",
"sizes": "1080x1920",
"type": "image/png"
}
]
}
部署脚本
#!/bin/bash
# scripts/deploy_web.sh
echo "开始构建Web应用..."
# 清理之前的构建
flutter clean
# 获取依赖
flutter pub get
# 构建Web应用
flutter build web --release --web-renderer canvaskit
# 部署到服务器(示例:使用rsync)
if [ "$1" = "production" ]; then
echo "部署到生产环境..."
rsync -avz --delete build/web/ user@server:/var/www/fitness-training/
elif [ "$1" = "staging" ]; then
echo "部署到测试环境..."
rsync -avz --delete build/web/ user@staging-server:/var/www/fitness-training-staging/
else
echo "Web构建完成!"
echo "构建文件位置: build/web/"
echo "使用 'flutter serve' 或部署到Web服务器"
fi
4. CI/CD配置
GitHub Actions配置
# .github/workflows/ci_cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- name: Install dependencies
run: flutter pub get
- name: Run code generation
run: flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Run tests
run: flutter test --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: coverage/lcov.info
build_android:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
- name: Install dependencies
run: flutter pub get
- name: Build APK
run: flutter build apk --release
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: app-release.apk
path: build/app/outputs/flutter-apk/app-release.apk
build_ios:
needs: test
runs-on: macos-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- name: Install dependencies
run: flutter pub get
- name: Build iOS
run: flutter build ios --release --no-codesign
build_web:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- name: Install dependencies
run: flutter pub get
- name: Build Web
run: flutter build web --release
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build/web
项目总结
健身训练计划应用是一个功能完整、设计精良的Flutter应用,展示了现代移动应用开发的最佳实践。通过本教程的学习,你将掌握:
技术收获
- Flutter框架深度应用:从基础Widget到复杂状态管理,全面掌握Flutter开发技能
- Material Design 3实践:现代化的UI设计语言应用,提供优秀的用户体验
- 数据架构设计:完整的数据模型设计,支持复杂的业务逻辑
- 性能优化技巧:内存管理、渲染优化、网络请求优化等实用技能
- 测试驱动开发:单元测试、Widget测试、集成测试的完整实践
业务价值
- 用户体验优先:直观的界面设计,流畅的交互体验
- 功能完整性:涵盖训练计划制定、执行、追踪的完整流程
- 数据驱动决策:详细的统计分析,帮助用户优化训练效果
- 个性化服务:基于用户数据的智能推荐和调整
- 扩展性设计:模块化架构,便于功能扩展和维护
开发经验
- 项目架构规划:清晰的代码结构,便于团队协作和维护
- 版本控制实践:Git工作流程,代码审查和持续集成
- 文档编写习惯:完整的技术文档,降低项目维护成本
- 用户反馈循环:基于用户需求的迭代开发流程
- 跨平台部署:Android、iOS、Web多平台发布经验
未来发展方向
- AI智能化:集成机器学习算法,提供更智能的训练建议
- 社交化功能:构建健身社区,增强用户粘性
- 硬件集成:支持更多可穿戴设备和健身器械
- 数据分析:深度挖掘用户数据,提供专业的健身指导
- 商业化探索:付费计划、教练服务、营养指导等增值服务
通过这个项目的开发,你不仅学会了Flutter技术栈的应用,更重要的是掌握了完整的产品开发流程。从需求分析到架构设计,从编码实现到测试部署,每个环节都体现了专业的软件开发素养。
希望这个健身训练计划应用能够成为你Flutter学习路上的重要里程碑,也期待你能够基于这个基础,开发出更多优秀的应用产品。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)