Flutter 框架跨平台鸿蒙开发 - 学习计划制定器开发教程
学习计划制定器是一个基于Flutter开发的学习管理应用,专门帮助学生和学习者制定、跟踪和管理学习计划。应用支持多种计划类型,涵盖11个学科分类,提供完整的学习进度跟踪和统计分析功能。运行效果图StudyPlannerAppStudyPlannerHomePage计划页面今日页面进度页面设置页面计划列表添加计划对话框今日任务列表任务状态管理总体进度统计科目进度分析时间统计StudyPlan数据模型
·
Flutter学习计划制定器开发教程
项目简介
学习计划制定器是一个基于Flutter开发的学习管理应用,专门帮助学生和学习者制定、跟踪和管理学习计划。应用支持多种计划类型,涵盖11个学科分类,提供完整的学习进度跟踪和统计分析功能。
运行效果图




核心功能
- 多类型学习计划:支持日计划、周计划、月计划和自定义计划
- 学科分类管理:涵盖数学、英语、物理等11个主要学科
- 任务状态跟踪:支持未开始、进行中、已完成、已暂停四种状态
- 智能进度统计:提供总体进度、科目进度和时间统计
- 今日任务视图:专门的今日学习任务管理界面
- 可视化进度:直观的进度条和圆形图表展示
技术特点
- 单文件架构,代码结构清晰
- Material Design 3设计风格
- 丰富的数据模型和状态管理
- 响应式布局适配不同屏幕尺寸
架构设计
整体架构
核心组件
| 组件 | 功能 | 说明 |
|---|---|---|
| StudyPlannerApp | 应用入口 | 配置主题和路由 |
| StudyPlannerHomePage | 主页面 | 管理页面状态和导航 |
| StudyPlan | 学习计划模型 | 封装计划信息和任务列表 |
| StudyTask | 学习任务模型 | 封装任务详情和进度信息 |
| StudyConfig | 配置管理类 | 定义枚举映射和颜色配置 |
数据模型设计
枚举类型
PlanType - 计划类型
enum PlanType {
daily, // 日计划
weekly, // 周计划
monthly, // 月计划
custom, // 自定义
}
Subject - 学习科目
enum Subject {
math, // 数学
english, // 英语
chinese, // 语文
physics, // 物理
chemistry, // 化学
biology, // 生物
history, // 历史
geography, // 地理
politics, // 政治
computer, // 计算机
other, // 其他
}
StudyStatus - 学习状态
enum StudyStatus {
notStarted, // 未开始
inProgress, // 进行中
completed, // 已完成
paused, // 已暂停
}
配置类
StudyConfig - 学习配置
class StudyConfig {
// 计划类型名称映射
static const Map<PlanType, String> planTypeNames = {
PlanType.daily: '日计划',
PlanType.weekly: '周计划',
PlanType.monthly: '月计划',
PlanType.custom: '自定义',
};
// 科目颜色配置
static const Map<Subject, Color> subjectColors = {
Subject.math: Colors.blue,
Subject.english: Colors.green,
Subject.chinese: Colors.red,
// ... 其他科目配置
};
// 状态颜色配置
static const Map<StudyStatus, Color> statusColors = {
StudyStatus.notStarted: Colors.grey,
StudyStatus.inProgress: Colors.blue,
StudyStatus.completed: Colors.green,
StudyStatus.paused: Colors.orange,
};
}
核心数据类
StudyTask - 学习任务模型
class StudyTask {
final String id; // 唯一标识
final String title; // 任务标题
final String description; // 任务描述
final Subject subject; // 学习科目
final DateTime startDate; // 开始日期
final DateTime endDate; // 结束日期
final int estimatedHours; // 预计学习时长
final StudyStatus status; // 任务状态
final int completedHours; // 已完成时长
final List<String> goals; // 学习目标
// 计算完成进度
double get progress {
if (estimatedHours == 0) return 0.0;
return (completedHours / estimatedHours).clamp(0.0, 1.0);
}
// 检查是否逾期
bool get isOverdue {
return DateTime.now().isAfter(endDate) && status != StudyStatus.completed;
}
// 剩余天数
int get daysLeft {
final now = DateTime.now();
if (now.isAfter(endDate)) return 0;
return endDate.difference(now).inDays;
}
}
StudyPlan - 学习计划模型
class StudyPlan {
final String id; // 唯一标识
final String title; // 计划标题
final String description; // 计划描述
final PlanType type; // 计划类型
final DateTime startDate; // 开始日期
final DateTime endDate; // 结束日期
final List<StudyTask> tasks; // 任务列表
final List<String> objectives; // 学习目标
// 计算总体进度
double get overallProgress {
if (tasks.isEmpty) return 0.0;
final totalProgress = tasks.fold(0.0, (sum, task) => sum + task.progress);
return totalProgress / tasks.length;
}
// 获取已完成任务数
int get completedTasksCount {
return tasks.where((task) => task.status == StudyStatus.completed).length;
}
// 获取进行中任务数
int get inProgressTasksCount {
return tasks.where((task) => task.status == StudyStatus.inProgress).length;
}
}
数据模型特点:
- 使用不可变数据结构,通过copyWith方法更新
- 内置计算属性,自动计算进度和状态
- 支持复杂的日期和时间逻辑
- 提供丰富的查询和统计方法
核心功能实现
1. 学习计划管理
创建学习计划
void _showPlanDialog({StudyPlan? plan}) {
final isEdit = plan != null;
// 控制器初始化
final titleController = TextEditingController(text: plan?.title ?? '');
final descController = TextEditingController(text: plan?.description ?? '');
PlanType selectedType = plan?.type ?? PlanType.weekly;
DateTime selectedStartDate = plan?.startDate ?? DateTime.now();
DateTime selectedEndDate = plan?.endDate ?? DateTime.now().add(const Duration(days: 7));
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(isEdit ? '编辑计划' : '创建学习计划'),
content: SingleChildScrollView(
child: Column(
children: [
// 标题输入
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: '计划标题',
border: OutlineInputBorder(),
),
),
// 计划类型选择
DropdownButtonFormField<PlanType>(
value: selectedType,
items: PlanType.values.map((type) {
return DropdownMenuItem(
value: type,
child: Text(StudyConfig.planTypeNames[type] ?? ''),
);
}).toList(),
onChanged: (value) {
setDialogState(() {
selectedType = value!;
// 根据类型自动设置结束日期
switch (value) {
case PlanType.daily:
selectedEndDate = selectedStartDate;
break;
case PlanType.weekly:
selectedEndDate = selectedStartDate.add(const Duration(days: 7));
break;
case PlanType.monthly:
selectedEndDate = selectedStartDate.add(const Duration(days: 30));
break;
case PlanType.custom:
break;
}
});
},
),
// 日期选择
Row(
children: [
Expanded(
child: ListTile(
title: const Text('开始日期'),
subtitle: Text(_formatDate(selectedStartDate)),
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: selectedStartDate,
firstDate: DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (date != null) {
setDialogState(() {
selectedStartDate = date;
});
}
},
),
),
// 结束日期选择...
],
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () => _savePlan(),
child: Text(isEdit ? '更新' : '创建'),
),
],
),
),
);
}
智能日期设置
// 根据计划类型自动设置结束日期
switch (selectedType) {
case PlanType.daily:
selectedEndDate = selectedStartDate;
break;
case PlanType.weekly:
selectedEndDate = selectedStartDate.add(const Duration(days: 7));
break;
case PlanType.monthly:
selectedEndDate = selectedStartDate.add(const Duration(days: 30));
break;
case PlanType.custom:
// 用户自定义,不自动设置
break;
}
2. 学习任务管理
添加学习任务
void _showTaskDialog(StudyPlan plan, {StudyTask? task}) {
final isEdit = task != null;
final titleController = TextEditingController(text: task?.title ?? '');
Subject selectedSubject = task?.subject ?? Subject.other;
int estimatedHours = task?.estimatedHours ?? 1;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(isEdit ? '编辑任务' : '添加学习任务'),
content: SingleChildScrollView(
child: Column(
children: [
// 任务标题
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: '任务标题',
border: OutlineInputBorder(),
),
),
// 科目选择
DropdownButtonFormField<Subject>(
value: selectedSubject,
decoration: const InputDecoration(
labelText: '学习科目',
border: OutlineInputBorder(),
),
items: Subject.values.map((subject) {
return DropdownMenuItem(
value: subject,
child: Row(
children: [
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: StudyConfig.subjectColors[subject],
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(StudyConfig.subjectNames[subject] ?? ''),
],
),
);
}).toList(),
onChanged: (value) {
setDialogState(() {
selectedSubject = value!;
});
},
),
// 预计时长
TextFormField(
initialValue: estimatedHours.toString(),
decoration: const InputDecoration(
labelText: '预计时长(小时)',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
onChanged: (value) {
estimatedHours = int.tryParse(value) ?? 1;
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () => _saveTask(),
child: Text(isEdit ? '更新' : '添加'),
),
],
),
),
);
}
任务状态更新
void _updateTaskStatus(StudyTask task, StudyStatus newStatus) {
setState(() {
for (int planIndex = 0; planIndex < _plans.length; planIndex++) {
final taskIndex = _plans[planIndex].tasks.indexWhere((t) => t.id == task.id);
if (taskIndex != -1) {
final updatedTasks = List<StudyTask>.from(_plans[planIndex].tasks);
updatedTasks[taskIndex] = task.copyWith(
status: newStatus,
completedHours: newStatus == StudyStatus.completed
? task.estimatedHours
: task.completedHours,
);
_plans[planIndex] = _plans[planIndex].copyWith(tasks: updatedTasks);
break;
}
}
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('任务状态已更新为:${StudyConfig.statusNames[newStatus]}')),
);
}
3. 今日任务筛选
今日任务算法
List<StudyTask> _getTodayTasks() {
final today = DateTime.now();
final todayStart = DateTime(today.year, today.month, today.day);
final todayEnd = DateTime(today.year, today.month, today.day, 23, 59, 59);
return _plans
.expand((plan) => plan.tasks)
.where((task) =>
// 任务时间范围与今天有交集
(task.startDate.isBefore(todayEnd) && task.endDate.isAfter(todayStart)) ||
// 任务开始日期是今天
(task.startDate.isAtSameMomentAs(todayStart)) ||
// 任务结束日期是今天
(task.endDate.isAtSameMomentAs(todayStart)))
.toList();
}
4. 进度统计算法
总体进度计算
double get overallProgress {
if (tasks.isEmpty) return 0.0;
final totalProgress = tasks.fold(0.0, (sum, task) => sum + task.progress);
return totalProgress / tasks.length;
}
科目进度统计
Widget _buildSubjectProgressCard() {
final allTasks = _plans.expand((plan) => plan.tasks).toList();
final subjectStats = <Subject, Map<String, int>>{};
// 统计每个科目的任务数量
for (final subject in Subject.values) {
final subjectTasks = allTasks.where((task) => task.subject == subject).toList();
final completed = subjectTasks.where((task) => task.status == StudyStatus.completed).length;
subjectStats[subject] = {
'total': subjectTasks.length,
'completed': completed,
};
}
return Card(
child: Column(
children: [
const Text('科目进度', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
// 只显示有任务的科目
...subjectStats.entries.where((entry) => entry.value['total']! > 0).map((entry) {
final subject = entry.key;
final total = entry.value['total']!;
final completed = entry.value['completed']!;
final progress = total > 0 ? completed / total : 0.0;
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(StudyConfig.subjectNames[subject] ?? ''),
Text('$completed/$total'),
],
),
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(
StudyConfig.subjectColors[subject] ?? Colors.grey,
),
),
],
);
}),
],
),
);
}
UI组件设计
1. 主页面布局
主页面采用底部导航栏设计,包含四个主要页面:
Widget build(BuildContext context) {
return Scaffold(
body: [
_buildPlansPage(), // 计划管理
_buildTodayPage(), // 今日学习
_buildProgressPage(), // 进度统计
_buildSettingsPage(), // 设置
][_selectedIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
destinations: const [
NavigationDestination(
icon: Icon(Icons.calendar_today_outlined),
selectedIcon: Icon(Icons.calendar_today),
label: '计划',
),
NavigationDestination(
icon: Icon(Icons.today_outlined),
selectedIcon: Icon(Icons.today),
label: '今日',
),
NavigationDestination(
icon: Icon(Icons.analytics_outlined),
selectedIcon: Icon(Icons.analytics),
label: '进度',
),
NavigationDestination(
icon: Icon(Icons.settings_outlined),
selectedIcon: Icon(Icons.settings),
label: '设置',
),
],
),
floatingActionButton: _selectedIndex == 0
? FloatingActionButton(
onPressed: _showAddPlanDialog,
child: const Icon(Icons.add),
)
: null,
);
}
2. 计划卡片设计
计划卡片组件
Widget _buildPlanCard(StudyPlan plan) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () => _showPlanDetails(plan),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题和类型标签
Row(
children: [
Expanded(
child: Text(
plan.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.teal.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(
StudyConfig.planTypeNames[plan.type] ?? '',
style: TextStyle(
fontSize: 12,
color: Colors.teal.shade700,
fontWeight: FontWeight.bold,
),
),
),
],
),
// 描述
if (plan.description.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
plan.description,
style: TextStyle(color: Colors.grey.shade600),
),
],
// 任务统计和日期
const SizedBox(height: 12),
Row(
children: [
Icon(Icons.task_alt, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 4),
Text(
'${plan.completedTasksCount}/${plan.tasks.length} 任务完成',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
const Spacer(),
Icon(Icons.schedule, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 4),
Text(
'${_formatDate(plan.startDate)} - ${_formatDate(plan.endDate)}',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
],
),
// 进度条
const SizedBox(height: 8),
LinearProgressIndicator(
value: plan.overallProgress,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(Colors.teal.shade400),
),
const SizedBox(height: 4),
Text(
'${(plan.overallProgress * 100).toStringAsFixed(1)}% 完成',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
],
),
),
),
);
}
3. 任务卡片设计
任务卡片组件
Widget _buildTaskCard(StudyTask task) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
// 科目头像
leading: CircleAvatar(
backgroundColor: StudyConfig.subjectColors[task.subject],
child: Text(
StudyConfig.subjectNames[task.subject]?.substring(0, 1) ?? '',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
// 任务标题
title: Text(task.title),
// 任务详情
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (task.description.isNotEmpty)
Text(task.description),
const SizedBox(height: 4),
Row(
children: [
// 状态标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: StudyConfig.statusColors[task.status],
borderRadius: BorderRadius.circular(10),
),
child: Text(
StudyConfig.statusNames[task.status] ?? '',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
// 时长信息
Text(
'${task.completedHours}/${task.estimatedHours}h',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
// 逾期警告
if (task.isOverdue) ...[
const SizedBox(width: 8),
const Icon(Icons.warning, size: 14, color: Colors.red),
const Text(
'逾期',
style: TextStyle(fontSize: 12, color: Colors.red),
),
],
],
),
],
),
// 操作菜单
trailing: PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
value: 'start',
child: const Row(
children: [
Icon(Icons.play_arrow, size: 18),
SizedBox(width: 8),
Text('开始学习'),
],
),
),
PopupMenuItem(
value: 'complete',
child: const Row(
children: [
Icon(Icons.check, size: 18),
SizedBox(width: 8),
Text('标记完成'),
],
),
),
PopupMenuItem(
value: 'edit',
child: const Row(
children: [
Icon(Icons.edit, size: 18),
SizedBox(width: 8),
Text('编辑'),
],
),
),
],
onSelected: (value) {
if (value == 'start') {
_updateTaskStatus(task, StudyStatus.inProgress);
} else if (value == 'complete') {
_updateTaskStatus(task, StudyStatus.completed);
} else if (value == 'edit') {
_showEditTaskDialog(task);
}
},
),
),
);
}
4. 统计图表组件
圆形进度图表
Widget _buildCircularProgress(int completed, int total) {
return Center(
child: SizedBox(
width: 120,
height: 120,
child: Stack(
children: [
CircularProgressIndicator(
value: total > 0 ? completed / total : 0,
strokeWidth: 12,
backgroundColor: Colors.grey.shade300,
valueColor: const AlwaysStoppedAnimation<Color>(Colors.teal),
),
Center(
child: Text(
'${total > 0 ? (completed / total * 100).toStringAsFixed(1) : 0}%',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
统计项组件
Widget _buildStatItem(String label, String value, Color color) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Text(
value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: color,
),
),
],
),
);
}
状态管理
1. 状态变量设计
class _StudyPlannerHomePageState extends State<StudyPlannerHomePage> {
int _selectedIndex = 0; // 当前选中的页面索引
List<StudyPlan> _plans = []; // 学习计划列表
}
2. 数据更新机制
计划更新
void _savePlan(StudyPlan plan, {bool isEdit = false}) {
setState(() {
if (isEdit) {
final index = _plans.indexWhere((p) => p.id == plan.id);
if (index != -1) {
_plans[index] = plan;
}
} else {
_plans.add(plan);
}
});
}
任务更新
void _saveTask(StudyPlan plan, StudyTask task, {bool isEdit = false}) {
setState(() {
final planIndex = _plans.indexWhere((p) => p.id == plan.id);
if (planIndex != -1) {
final updatedTasks = List<StudyTask>.from(_plans[planIndex].tasks);
if (isEdit) {
final taskIndex = updatedTasks.indexWhere((t) => t.id == task.id);
if (taskIndex != -1) {
updatedTasks[taskIndex] = task;
}
} else {
updatedTasks.add(task);
}
_plans[planIndex] = _plans[planIndex].copyWith(tasks: updatedTasks);
}
});
}
3. 不可变数据更新
使用copyWith模式确保数据不可变性:
StudyTask copyWith({
String? title,
String? description,
Subject? subject,
DateTime? startDate,
DateTime? endDate,
int? estimatedHours,
StudyStatus? status,
int? completedHours,
List<String>? goals,
}) {
return StudyTask(
id: id,
title: title ?? this.title,
description: description ?? this.description,
subject: subject ?? this.subject,
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
estimatedHours: estimatedHours ?? this.estimatedHours,
status: status ?? this.status,
completedHours: completedHours ?? this.completedHours,
goals: goals ?? this.goals,
);
}
工具方法实现
1. 日期处理工具
class DateUtils {
// 格式化日期为 YYYY-MM-DD 格式
static String formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
// 格式化今日日期(包含星期)
static String formatTodayDate() {
final now = DateTime.now();
final weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
final weekday = weekdays[now.weekday - 1];
return '${formatDate(now)} $weekday';
}
// 检查日期是否为今天
static bool isToday(DateTime date) {
final now = DateTime.now();
return date.year == now.year &&
date.month == now.month &&
date.day == now.day;
}
// 获取日期范围内的天数
static int getDaysBetween(DateTime start, DateTime end) {
return end.difference(start).inDays;
}
// 检查两个日期范围是否有交集
static bool hasDateOverlap(DateTime start1, DateTime end1, DateTime start2, DateTime end2) {
return start1.isBefore(end2) && end1.isAfter(start2);
}
}
2. 进度计算工具
class ProgressUtils {
// 计算任务完成率
static double calculateTaskProgress(int completedHours, int estimatedHours) {
if (estimatedHours == 0) return 0.0;
return (completedHours / estimatedHours).clamp(0.0, 1.0);
}
// 计算计划总体进度
static double calculatePlanProgress(List<StudyTask> tasks) {
if (tasks.isEmpty) return 0.0;
final totalProgress = tasks.fold(0.0, (sum, task) => sum + task.progress);
return totalProgress / tasks.length;
}
// 获取状态统计
static Map<StudyStatus, int> getStatusStatistics(List<StudyTask> tasks) {
final stats = <StudyStatus, int>{};
for (final status in StudyStatus.values) {
stats[status] = tasks.where((task) => task.status == status).length;
}
return stats;
}
// 获取科目统计
static Map<Subject, Map<String, int>> getSubjectStatistics(List<StudyTask> tasks) {
final stats = <Subject, Map<String, int>>{};
for (final subject in Subject.values) {
final subjectTasks = tasks.where((task) => task.subject == subject).toList();
final completed = subjectTasks.where((task) => task.status == StudyStatus.completed).length;
stats[subject] = {
'total': subjectTasks.length,
'completed': completed,
};
}
return stats;
}
}
3. 验证工具
class ValidationUtils {
// 验证计划标题
static String? validatePlanTitle(String? title) {
if (title == null || title.trim().isEmpty) {
return '请输入计划标题';
}
if (title.trim().length < 2) {
return '计划标题至少需要2个字符';
}
if (title.trim().length > 50) {
return '计划标题不能超过50个字符';
}
return null;
}
// 验证任务标题
static String? validateTaskTitle(String? title) {
if (title == null || title.trim().isEmpty) {
return '请输入任务标题';
}
if (title.trim().length < 2) {
return '任务标题至少需要2个字符';
}
return null;
}
// 验证学习时长
static String? validateStudyHours(String? hours) {
if (hours == null || hours.trim().isEmpty) {
return '请输入学习时长';
}
final hoursInt = int.tryParse(hours.trim());
if (hoursInt == null) {
return '请输入有效的数字';
}
if (hoursInt <= 0) {
return '学习时长必须大于0';
}
if (hoursInt > 24) {
return '单次学习时长不能超过24小时';
}
return null;
}
// 验证日期范围
static String? validateDateRange(DateTime startDate, DateTime endDate) {
if (endDate.isBefore(startDate)) {
return '结束日期不能早于开始日期';
}
final daysDiff = endDate.difference(startDate).inDays;
if (daysDiff > 365) {
return '计划时间跨度不能超过一年';
}
return null;
}
}
功能扩展建议
1. 数据持久化
// 使用 SharedPreferences 实现本地存储
class StudyPlanStorage {
static const String _plansKey = 'study_plans';
static Future<void> savePlans(List<StudyPlan> plans) async {
final prefs = await SharedPreferences.getInstance();
final plansJson = plans.map((plan) => plan.toJson()).toList();
await prefs.setString(_plansKey, jsonEncode(plansJson));
}
static Future<List<StudyPlan>> loadPlans() async {
final prefs = await SharedPreferences.getInstance();
final plansString = prefs.getString(_plansKey);
if (plansString == null) return [];
final plansJson = jsonDecode(plansString) as List;
return plansJson.map((json) => StudyPlan.fromJson(json)).toList();
}
}
// 扩展数据模型添加序列化方法
extension StudyPlanSerialization on StudyPlan {
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'type': type.index,
'startDate': startDate.millisecondsSinceEpoch,
'endDate': endDate.millisecondsSinceEpoch,
'tasks': tasks.map((task) => task.toJson()).toList(),
'objectives': objectives,
};
}
static StudyPlan fromJson(Map<String, dynamic> json) {
return StudyPlan(
id: json['id'],
title: json['title'],
description: json['description'] ?? '',
type: PlanType.values[json['type']],
startDate: DateTime.fromMillisecondsSinceEpoch(json['startDate']),
endDate: DateTime.fromMillisecondsSinceEpoch(json['endDate']),
tasks: (json['tasks'] as List).map((taskJson) => StudyTask.fromJson(taskJson)).toList(),
objectives: List<String>.from(json['objectives'] ?? []),
);
}
}
2. 学习提醒功能
class StudyReminderService {
static Future<void> scheduleStudyReminder(StudyTask task) async {
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
// 在任务开始时间提醒
await flutterLocalNotificationsPlugin.schedule(
task.id.hashCode,
'学习提醒',
'是时候开始学习"${task.title}"了!',
task.startDate,
const NotificationDetails(
android: AndroidNotificationDetails(
'study_reminder',
'学习提醒',
channelDescription: '学习任务开始提醒',
importance: Importance.high,
priority: Priority.high,
),
),
);
}
static Future<void> scheduleDailyReminder(TimeOfDay time) async {
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin.periodicallyShow(
0,
'每日学习提醒',
'今天的学习计划完成了吗?',
RepeatInterval.daily,
const NotificationDetails(
android: AndroidNotificationDetails(
'daily_reminder',
'每日提醒',
channelDescription: '每日学习提醒',
importance: Importance.medium,
priority: Priority.medium,
),
),
);
}
}
3. 学习统计增强
class StudyAnalytics {
// 获取学习时长统计
static Map<String, int> getWeeklyStudyHours(List<StudyTask> tasks) {
final weeklyHours = <String, int>{};
final now = DateTime.now();
for (int i = 6; i >= 0; i--) {
final date = now.subtract(Duration(days: i));
final dateKey = DateUtils.formatDate(date);
final dayTasks = tasks.where((task) =>
task.status == StudyStatus.completed &&
DateUtils.isToday(task.endDate)
).toList();
final totalHours = dayTasks.fold(0, (sum, task) => sum + task.completedHours);
weeklyHours[dateKey] = totalHours;
}
return weeklyHours;
}
// 获取学习效率分析
static Map<String, double> getStudyEfficiency(List<StudyTask> tasks) {
final completedTasks = tasks.where((task) => task.status == StudyStatus.completed).toList();
if (completedTasks.isEmpty) {
return {'efficiency': 0.0, 'onTimeRate': 0.0};
}
final totalEstimated = completedTasks.fold(0, (sum, task) => sum + task.estimatedHours);
final totalActual = completedTasks.fold(0, (sum, task) => sum + task.completedHours);
final onTimeTasks = completedTasks.where((task) => !task.isOverdue).length;
return {
'efficiency': totalEstimated > 0 ? totalActual / totalEstimated : 0.0,
'onTimeRate': onTimeTasks / completedTasks.length,
};
}
// 获取科目学习时长排行
static List<MapEntry<Subject, int>> getSubjectRanking(List<StudyTask> tasks) {
final subjectHours = <Subject, int>{};
for (final task in tasks.where((task) => task.status == StudyStatus.completed)) {
subjectHours[task.subject] = (subjectHours[task.subject] ?? 0) + task.completedHours;
}
final sortedEntries = subjectHours.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
return sortedEntries;
}
}
4. 学习目标设定
class StudyGoal {
final String id;
final String title;
final GoalType type;
final int targetValue;
final int currentValue;
final DateTime deadline;
final bool isCompleted;
const StudyGoal({
required this.id,
required this.title,
required this.type,
required this.targetValue,
this.currentValue = 0,
required this.deadline,
this.isCompleted = false,
});
double get progress => targetValue > 0 ? (currentValue / targetValue).clamp(0.0, 1.0) : 0.0;
}
enum GoalType {
dailyHours, // 每日学习时长
weeklyHours, // 每周学习时长
monthlyHours, // 每月学习时长
taskCount, // 任务完成数量
subjectHours, // 特定科目学习时长
}
class GoalManager {
static List<StudyGoal> _goals = [];
static void addGoal(StudyGoal goal) {
_goals.add(goal);
}
static void updateGoalProgress(String goalId, int newValue) {
final index = _goals.indexWhere((goal) => goal.id == goalId);
if (index != -1) {
final goal = _goals[index];
_goals[index] = StudyGoal(
id: goal.id,
title: goal.title,
type: goal.type,
targetValue: goal.targetValue,
currentValue: newValue,
deadline: goal.deadline,
isCompleted: newValue >= goal.targetValue,
);
}
}
static List<StudyGoal> getActiveGoals() {
return _goals.where((goal) => !goal.isCompleted && DateTime.now().isBefore(goal.deadline)).toList();
}
}
5. 学习计划模板
class StudyPlanTemplate {
final String id;
final String name;
final String description;
final PlanType type;
final List<StudyTaskTemplate> taskTemplates;
const StudyPlanTemplate({
required this.id,
required this.name,
required this.description,
required this.type,
required this.taskTemplates,
});
StudyPlan createPlan(DateTime startDate) {
final endDate = _calculateEndDate(startDate, type);
final tasks = taskTemplates.map((template) => template.createTask(startDate)).toList();
return StudyPlan(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: name,
description: description,
type: type,
startDate: startDate,
endDate: endDate,
tasks: tasks,
);
}
DateTime _calculateEndDate(DateTime startDate, PlanType type) {
switch (type) {
case PlanType.daily:
return startDate;
case PlanType.weekly:
return startDate.add(const Duration(days: 7));
case PlanType.monthly:
return startDate.add(const Duration(days: 30));
case PlanType.custom:
return startDate.add(const Duration(days: 14)); // 默认2周
}
}
}
class StudyTaskTemplate {
final String title;
final String description;
final Subject subject;
final int estimatedHours;
final int dayOffset; // 相对于计划开始日期的偏移天数
const StudyTaskTemplate({
required this.title,
required this.description,
required this.subject,
required this.estimatedHours,
this.dayOffset = 0,
});
StudyTask createTask(DateTime planStartDate) {
final taskStartDate = planStartDate.add(Duration(days: dayOffset));
final taskEndDate = taskStartDate.add(const Duration(days: 1));
return StudyTask(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
description: description,
subject: subject,
startDate: taskStartDate,
endDate: taskEndDate,
estimatedHours: estimatedHours,
);
}
}
class TemplateManager {
static const List<StudyPlanTemplate> defaultTemplates = [
StudyPlanTemplate(
id: 'exam_prep',
name: '期末考试复习',
description: '系统性的期末考试复习计划',
type: PlanType.weekly,
taskTemplates: [
StudyTaskTemplate(
title: '数学复习',
description: '复习重点章节和练习题',
subject: Subject.math,
estimatedHours: 3,
dayOffset: 0,
),
StudyTaskTemplate(
title: '英语单词背诵',
description: '背诵重点词汇',
subject: Subject.english,
estimatedHours: 2,
dayOffset: 1,
),
// 更多任务模板...
],
),
// 更多计划模板...
];
}
性能优化策略
1. 列表优化
// 使用 ListView.builder 优化长列表性能
Widget _buildOptimizedPlanList(List<StudyPlan> plans) {
return ListView.builder(
itemCount: plans.length,
itemBuilder: (context, index) {
final plan = plans[index];
return _buildPlanCard(plan);
},
);
}
// 使用 SliverList 优化复杂滚动
Widget _buildSliverPlanList(List<StudyPlan> plans) {
return CustomScrollView(
slivers: [
SliverAppBar(
title: const Text('学习计划'),
floating: true,
snap: true,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _buildPlanCard(plans[index]),
childCount: plans.length,
),
),
],
);
}
2. 状态管理优化
// 使用 ChangeNotifier 优化状态管理
class StudyPlanNotifier extends ChangeNotifier {
List<StudyPlan> _plans = [];
List<StudyPlan> get plans => _plans;
void addPlan(StudyPlan plan) {
_plans.add(plan);
notifyListeners();
}
void updatePlan(String id, StudyPlan updatedPlan) {
final index = _plans.indexWhere((plan) => plan.id == id);
if (index != -1) {
_plans[index] = updatedPlan;
notifyListeners();
}
}
void deletePlan(String id) {
_plans.removeWhere((plan) => plan.id == id);
notifyListeners();
}
}
// 在 Widget 中使用
class StudyPlanWidget extends StatelessWidget {
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => StudyPlanNotifier(),
child: Consumer<StudyPlanNotifier>(
builder: (context, notifier, child) {
return ListView.builder(
itemCount: notifier.plans.length,
itemBuilder: (context, index) => _buildPlanCard(notifier.plans[index]),
);
},
),
);
}
}
3. 内存管理
class StudyPlanMemoryManager {
static const int maxPlansInMemory = 100;
static const int maxTasksPerPlan = 50;
// 清理过期的已完成计划
static List<StudyPlan> cleanupExpiredPlans(List<StudyPlan> plans) {
final now = DateTime.now();
final thirtyDaysAgo = now.subtract(const Duration(days: 30));
return plans.where((plan) {
// 保留未完成的计划
if (plan.overallProgress < 1.0) return true;
// 保留最近30天内的已完成计划
return plan.endDate.isAfter(thirtyDaysAgo);
}).toList();
}
// 限制内存中的计划数量
static List<StudyPlan> limitPlansInMemory(List<StudyPlan> plans) {
if (plans.length <= maxPlansInMemory) return plans;
// 按结束日期排序,保留最新的计划
final sortedPlans = List<StudyPlan>.from(plans)
..sort((a, b) => b.endDate.compareTo(a.endDate));
return sortedPlans.take(maxPlansInMemory).toList();
}
// 限制每个计划的任务数量
static StudyPlan limitTasksInPlan(StudyPlan plan) {
if (plan.tasks.length <= maxTasksPerPlan) return plan;
// 按状态和日期排序,优先保留未完成和最新的任务
final sortedTasks = List<StudyTask>.from(plan.tasks)
..sort((a, b) {
// 未完成的任务优先
if (a.status != StudyStatus.completed && b.status == StudyStatus.completed) return -1;
if (a.status == StudyStatus.completed && b.status != StudyStatus.completed) return 1;
// 按结束日期排序
return b.endDate.compareTo(a.endDate);
});
return plan.copyWith(tasks: sortedTasks.take(maxTasksPerPlan).toList());
}
}
测试指南
1. 单元测试
import 'package:flutter_test/flutter_test.dart';
void main() {
group('StudyTask Tests', () {
test('progress calculation should be correct', () {
final task = StudyTask(
id: '1',
title: 'Test Task',
subject: Subject.math,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 1)),
estimatedHours: 4,
completedHours: 2,
);
expect(task.progress, 0.5);
});
test('isOverdue should work correctly', () {
final overduetask = StudyTask(
id: '1',
title: 'Overdue Task',
subject: Subject.math,
startDate: DateTime.now().subtract(const Duration(days: 2)),
endDate: DateTime.now().subtract(const Duration(days: 1)),
status: StudyStatus.inProgress,
);
expect(overduetask.isOverdue, true);
final completedTask = overduetask.copyWith(status: StudyStatus.completed);
expect(completedTask.isOverdue, false);
});
test('daysLeft calculation should be correct', () {
final task = StudyTask(
id: '1',
title: 'Future Task',
subject: Subject.math,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 3)),
);
expect(task.daysLeft, 3);
});
});
group('StudyPlan Tests', () {
test('overallProgress should be calculated correctly', () {
final tasks = [
StudyTask(
id: '1',
title: 'Task 1',
subject: Subject.math,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 1)),
estimatedHours: 2,
completedHours: 2, // 100% complete
),
StudyTask(
id: '2',
title: 'Task 2',
subject: Subject.english,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 1)),
estimatedHours: 4,
completedHours: 2, // 50% complete
),
];
final plan = StudyPlan(
id: '1',
title: 'Test Plan',
type: PlanType.weekly,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 7)),
tasks: tasks,
);
expect(plan.overallProgress, 0.75); // (1.0 + 0.5) / 2
});
test('task counts should be correct', () {
final tasks = [
StudyTask(
id: '1',
title: 'Completed Task',
subject: Subject.math,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 1)),
status: StudyStatus.completed,
),
StudyTask(
id: '2',
title: 'In Progress Task',
subject: Subject.english,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 1)),
status: StudyStatus.inProgress,
),
StudyTask(
id: '3',
title: 'Not Started Task',
subject: Subject.physics,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 1)),
status: StudyStatus.notStarted,
),
];
final plan = StudyPlan(
id: '1',
title: 'Test Plan',
type: PlanType.weekly,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 7)),
tasks: tasks,
);
expect(plan.completedTasksCount, 1);
expect(plan.inProgressTasksCount, 1);
});
});
}
2. Widget 测试
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('StudyPlanCard Widget Tests', () {
testWidgets('should display plan information correctly', (tester) async {
final plan = StudyPlan(
id: '1',
title: 'Test Plan',
description: 'Test Description',
type: PlanType.weekly,
startDate: DateTime(2024, 1, 1),
endDate: DateTime(2024, 1, 7),
tasks: [],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StudyPlanCard(plan: plan),
),
),
);
expect(find.text('Test Plan'), findsOneWidget);
expect(find.text('Test Description'), findsOneWidget);
expect(find.text('周计划'), findsOneWidget);
});
testWidgets('should show progress correctly', (tester) async {
final tasks = [
StudyTask(
id: '1',
title: 'Task 1',
subject: Subject.math,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 1)),
estimatedHours: 2,
completedHours: 2,
status: StudyStatus.completed,
),
];
final plan = StudyPlan(
id: '1',
title: 'Test Plan',
type: PlanType.weekly,
startDate: DateTime.now(),
endDate: DateTime.now().add(const Duration(days: 7)),
tasks: tasks,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StudyPlanCard(plan: plan),
),
),
);
expect(find.text('1/1 任务完成'), findsOneWidget);
expect(find.text('100.0% 完成'), findsOneWidget);
});
});
}
3. 集成测试
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Study Planner Integration Tests', () {
testWidgets('complete study plan workflow', (tester) async {
await tester.pumpWidget(const StudyPlannerApp());
// 1. 验证初始状态
expect(find.text('学习计划制定器'), findsOneWidget);
// 2. 创建新计划
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).first, '测试学习计划');
await tester.tap(find.text('创建'));
await tester.pumpAndSettle();
// 3. 验证计划已创建
expect(find.text('测试学习计划'), findsOneWidget);
// 4. 添加学习任务
await tester.tap(find.text('测试学习计划'));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.add_task));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).first, '测试任务');
await tester.tap(find.text('添加'));
await tester.pumpAndSettle();
// 5. 验证任务已添加
expect(find.text('测试任务'), findsOneWidget);
// 6. 切换到今日页面
await tester.tap(find.text('今日'));
await tester.pumpAndSettle();
// 7. 验证今日任务显示
expect(find.text('今日学习'), findsOneWidget);
// 8. 切换到进度页面
await tester.tap(find.text('进度'));
await tester.pumpAndSettle();
// 9. 验证进度统计
expect(find.text('总体进度'), findsOneWidget);
});
});
}
部署指南
1. Android 部署
权限配置 (android/app/src/main/AndroidManifest.xml)
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
构建配置 (android/app/build.gradle)
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.study_planner"
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "1.0.0"
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
2. iOS 部署
权限配置 (ios/Runner/Info.plist)
<key>NSUserNotificationUsageDescription</key>
<string>需要通知权限来提醒您的学习计划</string>
<key>NSCalendarsUsageDescription</key>
<string>需要日历权限来管理学习计划</string>
3. 构建命令
# 清理项目
flutter clean
# 获取依赖
flutter pub get
# Android APK (调试版)
flutter build apk --debug
# Android APK (发布版)
flutter build apk --release
# Android App Bundle (推荐用于 Google Play)
flutter build appbundle --release
# iOS (发布版)
flutter build ios --release
项目总结
学习计划制定器是一个功能完整的学习管理应用,展示了以下技术要点:
技术亮点
- 丰富的数据模型:完整的学习计划和任务数据结构设计
- 智能进度计算:自动计算任务和计划的完成进度
- 多维度统计:提供总体、科目、时间等多种统计维度
- 灵活的状态管理:支持复杂的任务状态转换和更新
- 直观的UI设计:清晰的信息层次和视觉反馈
学习价值
- 数据建模:学习复杂业务数据的建模和关系设计
- 状态管理:掌握Flutter中的状态管理最佳实践
- UI设计:了解学习类应用的界面设计原则
- 算法实现:学习进度计算和统计分析算法
扩展方向
- 数据持久化:集成本地数据库和云端同步
- 智能提醒:基于学习习惯的智能提醒系统
- 社交功能:学习小组和进度分享功能
- AI助手:基于学习数据的个性化建议
- 多平台同步:支持Web和桌面端同步
这个项目为学习Flutter应用开发提供了完整的实践案例,涵盖了数据建模、状态管理、UI设计、算法实现等多个方面,是一个优秀的学习和参考项目。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)