flutter_for_openharmony智慧学习助手app实战:每日计划功能实现
本文介绍了一个基于Flutter实现的每日学习计划功能。该功能通过任务列表帮助用户管理学习任务,包含添加、编辑、标记完成等核心功能。页面采用Card和ListTile组件构建美观的任务列表项,支持复选框标记完成状态和删除线样式。通过浮动按钮触发对话框实现任务添加功能,使用TextEditingController处理用户输入。整个界面简洁直观,便于用户快速查看和管理当日学习计划,有效提升学习效率。

学习计划是时间管理的重要工具。通过制定每日计划,用户可以明确当天的学习任务,合理安排时间,提高学习效率。一个好的计划管理功能应该简单易用,让用户可以快速添加任务、标记完成状态、查看进度。在这篇文章中,我们将实现一个实用的每日计划功能。
功能需求分析
每日计划功能的核心是任务列表。用户可以看到当天的所有学习任务,每个任务包含标题、时间段和完成状态。用户可以通过勾选复选框来标记任务完成,已完成的任务会显示删除线。
任务列表应该支持添加新任务。点击页面底部的浮动按钮,弹出对话框让用户输入任务信息。任务信息包括标题和时间段,这些是必填项。
任务还应该支持编辑和删除。点击任务右侧的更多按钮,显示操作菜单,用户可以选择编辑或删除。编辑功能弹出对话框,预填充当前任务信息,用户修改后保存。
任务的排序也很重要。未完成的任务应该显示在前面,已完成的任务显示在后面。在同一状态内,按照时间顺序排列。这种排序方式让用户可以专注于未完成的任务。
页面结构设计
每日计划页面使用StatefulWidget实现,因为任务列表的状态会频繁变化。用户勾选复选框、添加任务、删除任务都会改变状态,需要重新渲染页面。
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class DailyPlanPage extends StatefulWidget {
const DailyPlanPage({super.key});
State<DailyPlanPage> createState() => _DailyPlanPageState();
}
class _DailyPlanPageState extends State<DailyPlanPage> {
final List<Map<String, dynamic>> _plans = [
{'title': '复习高数第三章', 'time': '09:00-10:30', 'completed': true},
{'title': '背诵英语单词100个', 'time': '10:30-11:00', 'completed': true},
{'title': '完成物理作业', 'time': '14:00-16:00', 'completed': false},
{'title': '观看编程视频教程', 'time': '19:00-20:30', 'completed': false},
];
导入语句很简洁,只需要Flutter的基础包和ScreenUtil。这个页面不需要导航到其他页面,所以不需要GetX。
_plans列表存储所有的任务数据。这里使用Map来表示任务,包含title、time和completed三个字段。在实际应用中,应该定义一个Task类来表示任务,这样更规范,也有类型安全。
初始数据包含四个任务,其中两个已完成,两个未完成。这些数据用于演示,实际应用中应该从数据库加载。
页面布局实现
页面使用Scaffold提供标准布局,包含AppBar、Body和FloatingActionButton三个部分。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('每日计划'),
),
body: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: _plans.length,
itemBuilder: (context, index) {
final plan = _plans[index];
return Card(
margin: EdgeInsets.only(bottom: 12.h),
child: ListTile(
leading: Checkbox(
value: plan['completed'],
onChanged: (value) {
setState(() {
_plans[index]['completed'] = value;
});
},
),
title: Text(
plan['title'],
style: TextStyle(
decoration: plan['completed'] ? TextDecoration.lineThrough : null,
),
),
subtitle: Text(plan['time']),
trailing: IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {},
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
AppBar只包含标题,没有其他按钮。标题使用const构造函数,这是一个性能优化的小技巧。
Body使用ListView.builder构建任务列表。builder方式只构建可见的列表项,对于长列表性能更好。虽然每日计划通常不会有很多任务,但使用builder是一个好习惯。
padding设置列表的内边距,让内容不会紧贴屏幕边缘。itemCount指定列表项的数量,itemBuilder是构建每个列表项的函数。
FloatingActionButton是Material Design中的标准组件,用于主要操作。这里用于添加新任务,图标是加号,含义很明确。
任务列表项的实现
每个任务使用Card包裹,Card提供了阴影和圆角,让列表项看起来像卡片。
return Card(
margin: EdgeInsets.only(bottom: 12.h),
child: ListTile(
leading: Checkbox(
value: plan['completed'],
onChanged: (value) {
setState(() {
_plans[index]['completed'] = value;
});
},
),
title: Text(
plan['title'],
style: TextStyle(
decoration: plan['completed'] ? TextDecoration.lineThrough : null,
),
),
subtitle: Text(plan['time']),
trailing: IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {},
),
),
);
Card的margin只设置底部边距,让卡片之间有间隔。如果设置四周边距,左右会有多余的空白。
ListTile是Flutter提供的标准列表项组件,它自动处理了布局和间距。leading是左侧的组件,title是主标题,subtitle是副标题,trailing是右侧的组件。
Checkbox显示任务的完成状态。value参数绑定到任务的completed字段,onChanged回调在用户点击时触发。回调中使用setState更新状态,这会触发页面重建,复选框的状态就会更新。
标题文字根据完成状态显示不同的样式。如果任务已完成,添加删除线装饰。这种视觉反馈让用户清楚地看到哪些任务已完成。
副标题显示任务的时间段。时间信息帮助用户安排一天的学习节奏,避免任务冲突。
trailing是一个更多按钮,点击后可以显示编辑和删除选项。虽然目前点击没有实现功能,但预留了这个入口。
添加任务功能
点击浮动按钮应该弹出对话框,让用户输入新任务的信息。
void _showAddTaskDialog() {
final titleController = TextEditingController();
final timeController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('添加任务'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: '任务标题',
hintText: '请输入任务标题',
),
autofocus: true,
),
SizedBox(height: 16.h),
TextField(
controller: timeController,
decoration: const InputDecoration(
labelText: '时间段',
hintText: '例如:09:00-10:30',
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
if (titleController.text.isNotEmpty && timeController.text.isNotEmpty) {
setState(() {
_plans.add({
'title': titleController.text,
'time': timeController.text,
'completed': false,
});
});
Navigator.pop(context);
}
},
child: const Text('添加'),
),
],
),
);
}
TextEditingController用于获取输入框的内容。每个输入框需要一个独立的controller。在对话框关闭后,这些controller会被自动释放,不需要手动dispose。
showDialog显示对话框。builder参数返回对话框的内容,这里使用AlertDialog,它是Material Design的标准对话框。
对话框的content使用Column垂直排列两个输入框。mainAxisSize设置为min让Column只占用需要的高度,而不是填满整个对话框。
第一个输入框设置autofocus为true,对话框打开时自动获得焦点,用户可以直接输入。这个小细节可以提升用户体验。
输入框使用decoration设置标签和提示文字。labelText是浮动标签,当输入框获得焦点时会浮动到上方。hintText是占位符,显示输入示例。
对话框底部有两个按钮:取消和添加。取消按钮直接关闭对话框,不做任何操作。添加按钮先验证输入是否为空,如果不为空,将新任务添加到列表,然后关闭对话框。
添加任务时使用setState,这会触发页面重建,新任务就会显示在列表中。新任务的completed字段默认为false,表示未完成。
完成状态切换
用户点击复选框可以切换任务的完成状态。这个功能已经在列表项中实现了。
Checkbox(
value: plan['completed'],
onChanged: (value) {
setState(() {
_plans[index]['completed'] = value;
});
},
),
onChanged回调接收一个布尔值参数,表示新的状态。我们直接将这个值赋给任务的completed字段。
使用setState包裹状态更新很重要。如果不使用setState,虽然数据会改变,但页面不会重建,用户看不到变化。
当任务状态改变时,标题的样式也会改变。已完成的任务显示删除线,未完成的任务正常显示。这是通过条件表达式实现的。
decoration: plan['completed'] ? TextDecoration.lineThrough : null,
这种即时的视觉反馈让用户清楚地知道操作成功了。相比于显示一个提示消息,这种方式更自然。
编辑任务功能
点击更多按钮应该显示操作菜单,包含编辑和删除选项。
void _showTaskMenu(int index) {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.edit),
title: const Text('编辑'),
onTap: () {
Navigator.pop(context);
_showEditTaskDialog(index);
},
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: const Text('删除', style: TextStyle(color: Colors.red)),
onTap: () {
Navigator.pop(context);
_deleteTask(index);
},
),
],
),
),
);
}
showModalBottomSheet从底部弹出一个面板,这是移动应用中常见的交互模式。相比于对话框,底部面板更容易操作,特别是在大屏手机上。
SafeArea确保内容不会被系统UI遮挡,比如底部的手势条。这在全面屏手机上特别重要。
面板包含两个选项:编辑和删除。每个选项使用ListTile,包含图标和文字。删除选项使用红色,这是危险操作的标准颜色。
点击选项时,先关闭面板,然后执行相应的操作。如果不先关闭面板,会出现两个对话框叠加的情况。
编辑功能弹出对话框,预填充当前任务的信息。
void _showEditTaskDialog(int index) {
final plan = _plans[index];
final titleController = TextEditingController(text: plan['title']);
final timeController = TextEditingController(text: plan['time']);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('编辑任务'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(labelText: '任务标题'),
),
SizedBox(height: 16.h),
TextField(
controller: timeController,
decoration: const InputDecoration(labelText: '时间段'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
if (titleController.text.isNotEmpty && timeController.text.isNotEmpty) {
setState(() {
_plans[index]['title'] = titleController.text;
_plans[index]['time'] = timeController.text;
});
Navigator.pop(context);
}
},
child: const Text('保存'),
),
],
),
);
}
编辑对话框与添加对话框类似,主要区别是TextEditingController初始化时传入了当前的值。这样对话框打开时,输入框会显示当前的任务信息。
保存按钮点击时,更新对应索引的任务数据。使用setState触发页面重建,修改后的任务就会显示在列表中。
删除任务功能
删除任务需要用户确认,避免误操作。
void _deleteTask(int index) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除任务"${_plans[index]['title']}"吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
setState(() {
_plans.removeAt(index);
});
Navigator.pop(context);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('删除'),
),
],
),
);
}
确认对话框显示要删除的任务标题,让用户明确知道将要删除什么。这比简单地问"确定删除吗"更清楚。
删除按钮使用红色,强调这是危险操作。用户看到红色会更加谨慎。
确认后,使用removeAt方法从列表中删除指定索引的任务。同样需要使用setState触发页面重建。
任务排序优化
当前的任务列表按照添加顺序显示,但更好的方式是将未完成的任务显示在前面。
List<Map<String, dynamic>> get _sortedPlans {
final plans = List<Map<String, dynamic>>.from(_plans);
plans.sort((a, b) {
if (a['completed'] == b['completed']) {
return a['time'].compareTo(b['time']);
}
return a['completed'] ? 1 : -1;
});
return plans;
}
这个getter方法返回排序后的任务列表。首先复制原列表,避免修改原数据。然后使用sort方法排序。
排序规则是:如果两个任务的完成状态相同,按时间排序;否则,未完成的任务排在前面。
在ListView.builder中使用排序后的列表:
ListView.builder(
itemCount: _sortedPlans.length,
itemBuilder: (context, index) {
final plan = _sortedPlans[index];
// ...
},
)
这样用户总是先看到未完成的任务,可以专注于当前需要做的事情。
数据持久化
当前的任务数据只存在于内存中,应用关闭后就会丢失。实际应用中应该将数据保存到本地数据库。
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class TaskDatabase {
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'tasks.db');
return await openDatabase(
path,
version: 1,
onCreate: (db, version) {
return db.execute(
'CREATE TABLE tasks(id INTEGER PRIMARY KEY, title TEXT, time TEXT, completed INTEGER)',
);
},
);
}
Future<void> insertTask(Map<String, dynamic> task) async {
final db = await database;
await db.insert('tasks', task);
}
Future<List<Map<String, dynamic>>> getTasks() async {
final db = await database;
return await db.query('tasks');
}
Future<void> updateTask(int id, Map<String, dynamic> task) async {
final db = await database;
await db.update('tasks', task, where: 'id = ?', whereArgs: [id]);
}
Future<void> deleteTask(int id) async {
final db = await database;
await db.delete('tasks', where: 'id = ?', whereArgs: [id]);
}
}
这个数据库类封装了所有的数据库操作。使用单例模式确保只有一个数据库实例。
在页面初始化时从数据库加载任务:
void initState() {
super.initState();
_loadTasks();
}
Future<void> _loadTasks() async {
final db = TaskDatabase();
final tasks = await db.getTasks();
setState(() {
_plans = tasks;
});
}
添加、编辑、删除任务时,同时更新数据库:
void _addTask(String title, String time) async {
final task = {'title': title, 'time': time, 'completed': 0};
final db = TaskDatabase();
final id = await db.insertTask(task);
setState(() {
_plans.add({...task, 'id': id});
});
}
这样任务数据就会被持久化保存,应用重启后仍然可以看到之前的任务。
用户体验优化
添加一些细节可以提升用户体验。
当任务列表为空时,显示一个提示信息:
body: _plans.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.task_alt, size: 80.sp, color: Colors.grey[300]),
SizedBox(height: 16.h),
Text(
'还没有任务',
style: TextStyle(fontSize: 16.sp, color: Colors.grey),
),
SizedBox(height: 8.h),
Text(
'点击右下角按钮添加任务',
style: TextStyle(fontSize: 14.sp, color: Colors.grey[400]),
),
],
),
)
: ListView.builder(/* ... */),
空状态页面让用户知道如何开始使用功能,比空白页面更友好。
添加任务成功后,可以显示一个提示:
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('任务添加成功')),
);
SnackBar是Material Design的标准提示组件,从底部弹出,几秒后自动消失。
完成所有任务时,可以显示一个祝贺动画:
if (_plans.every((plan) => plan['completed'])) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('🎉 太棒了!'),
content: const Text('今天的所有任务都完成了!'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('继续加油'),
),
],
),
);
}
这种正向反馈可以激励用户持续使用应用。
总结
通过这篇文章,我们实现了每日计划功能。用户可以添加、编辑、删除任务,标记任务完成状态。任务列表会自动排序,将未完成的任务显示在前面。
我们使用了StatefulWidget管理页面状态,使用ListView.builder构建任务列表,使用对话框和底部面板实现交互。我们还讨论了数据持久化和用户体验优化。
在下一篇文章中,我们将实现学习计时器功能。计时器可以记录学习时长,帮助用户量化学习投入。这个功能涉及到Timer的使用、时间格式化、数据统计等知识点。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)