前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
todo_list 是一个基于 Flutter 的 Todo List 待办清单应用 。项目代码集中在 lib/main.dart,同时保留了 ohos 平台工程目录,适合用来讲解 Flutter 小应用如何在 OpenHarmony 工程中完成基础适配、运行和验证。
这篇文章不是泛泛地写一个待办清单 Demo,而是围绕当前项目的真实代码展开:TodoListApp 负责应用入口,Todo 表达任务数据,TodoListHomePage 承载页面状态,_addTodoFromInput、_showAddTodoDialog、_toggleTodo、_deleteTodo 共同组成新增、勾选、删除的业务闭环。
本文适合以下读者:
- 想用 Flutter 快速完成一个轻量级工具应用的开发者。
- 正在了解 Flutter 适配 OpenHarmony 工程结构的同学。
- 希望把一个小项目整理成 CSDN 技术文章、项目文档或作品集材料的读者。
本文重点回答三个问题:
- Flutter 待办清单应用如何组织数据模型、状态和 UI。
TextEditingController、ListView.builder、Checkbox、AlertDialog如何协同完成交互。- 将 Flutter 项目放到 OpenHarmony 工程中时,发布前应该检查哪些配置和验证项。
效果图如下:

一、项目定位与功能概览
1.1 项目目标
todo_list 的目标很清晰:让用户输入一条任务,添加到列表中,然后可以勾选完成或删除任务。它虽然是一个入门级应用,但覆盖了 Flutter 开发中非常关键的几类能力:
- 输入处理 :读取用户在
TextField中输入的文本。 - 状态管理 :使用
StatefulWidget和setState刷新页面。 - 列表渲染 :通过
ListView.builder动态渲染任务列表。 - 条件样式:任务完成后显示删除线和灰色文本。
- 弹窗交互 :空输入时弹出
AlertDialog,也可以通过 FAB 直接新增任务。 - 资源释放 :在
dispose()中释放控制器,避免生命周期问题。
1.2 当前功能清单
| 功能 | 当前实现方式 | 对应代码位置 |
|---|---|---|
| 输入任务 | TextField + _controller |
body 顶部输入区 |
| 添加任务 | _addTodoFromInput() 和 _addTodo() |
状态类方法 |
| 空输入弹窗 | _showAddTodoDialog() |
AlertDialog |
| 弹窗提交 | _submitDialogTodo() |
弹窗按钮和回车 |
| 勾选完成 | _toggleTodo(index) |
Checkbox.onChanged |
| 删除任务 | _deleteTodo(index) |
IconButton.onPressed |
| 空列表提示 | _todos.isEmpty ? Center(...) : ListView.builder(...) |
列表区域 |
1.3 为什么这个项目适合写成 CSDN 实战文章
Todo List 是经典案例,但它并不只是"增删改查"的玩具项目。一个任务从输入框进入应用,会经历以下完整链路:
- 用户输入文本。
- 控制器读取并
trim()。 - 生成
Todo数据对象。 - 将对象加入
_todos列表。 setState通知 Flutter 重建 UI。ListView.builder根据最新列表渲染任务卡片。- 用户通过复选框或删除按钮继续改变状态。
提示:写这类项目文章时,不要只说"实现了增删改查",而要把 数据如何流动 、状态如何刷新 、UI 如何响应 讲清楚,这样文章更有技术含量。
二、项目目录结构分析
2.1 根目录结构
当前项目遵循 Flutter 标准工程结构,并增加了 OpenHarmony 平台目录。
bash
todo_list/
├── lib/
│ └── main.dart
├── ohos/
│ ├── AppScope/
│ └── entry/
├── test/
│ └── widget_test.dart
├── pubspec.yaml
├── analysis_options.yaml
└── README.md
2.2 核心文件说明
| 文件/目录 | 作用 | 本文关注点 |
|---|---|---|
lib/main.dart |
Flutter 应用入口、页面、模型和交互逻辑 | 重点拆解 |
pubspec.yaml |
项目名称、SDK 约束、依赖配置 | 解释轻依赖设计 |
analysis_options.yaml |
Dart 静态分析规则 | 保持代码规范 |
test/ |
测试目录 | 可补充 Widget 测试 |
ohos/ |
OpenHarmony 平台工程 | 检查平台入口和资源 |
2.3 OpenHarmony 目录说明
ohos 目录说明该 Flutter 项目已经具备 OpenHarmony 平台承载结构,常见关键文件如下:
| OpenHarmony 文件 | 作用 | 发布前检查建议 |
|---|---|---|
AppScope/app.json5 |
应用级配置 | 检查应用名称、图标和 bundle 信息 |
entry/src/main/module.json5 |
模块级配置 | 检查入口 Ability 和页面配置 |
EntryAbility.ets |
Ability 生命周期入口 | 确认启动链路正常 |
Index.ets |
页面承载入口 | 确认 Flutter 页面挂载正常 |
GeneratedPluginRegistrant.ets |
插件注册文件 | 当前依赖简单,重点确认无异常插件 |
三、环境准备与依赖配置
3.1 pubspec 基础配置
项目名称是 todo_list,版本号是 1.0.0+1,Dart SDK 约束是 ^3.9.2。
yaml
name: todo_list
description: "A new Flutter project."
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.9.2
publish_to: 'none' 表示这是一个私有应用项目,不会被误发布到 pub.dev。如果你后续要整理成开源模板,可以保留这个配置;如果要发布 Dart package,则需要按包规范重新调整。
3.2 项目依赖
当前项目依赖非常轻量。
yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
对于 Todo List 应用来说,当前功能不需要数据库、网络库或第三方状态管理库。这样做的优点是学习成本低,读者可以把注意力集中在 Flutter 原生 Widget 和状态刷新机制上。
3.3 环境检查命令
bash
flutter --version
dart --version
flutter doctor
flutter pub get
如果你需要验证 OpenHarmony 侧工程,还需要使用 DevEco Studio 打开 ohos 目录,并确认本地 OpenHarmony SDK、签名配置和模拟器或真机环境正常。
四、应用入口源码拆解
4.1 main 函数
Flutter 应用从 main() 进入,当前项目直接启动 TodoListApp。
dart
import 'package:flutter/material.dart';
void main() {
runApp(const TodoListApp());
}
这段代码非常标准。runApp 接收一个 Widget,并将它作为整个 Flutter 渲染树的根节点。
4.2 TodoListApp
TodoListApp 是 StatelessWidget,因为它只负责应用级配置,不直接保存任务列表。
dart
class TodoListApp extends StatelessWidget {
const TodoListApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo List',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: true,
),
home: const TodoListHomePage(title: 'Todo List'),
);
}
}
这里有三个重点:
MaterialApp提供 Material 应用基础能力。ColorScheme.fromSeed(seedColor: Colors.green)使用绿色生成主题色。home指向TodoListHomePage,真正的业务状态从首页开始。
4.3 Material 3 风格
项目启用了 useMaterial3: true。这会影响 TextField、ElevatedButton、Card、Checkbox、FloatingActionButton 等组件的默认视觉风格。
注意:如果你的目标是多端一致体验,建议在真机上分别检查字体、按钮高度、输入框圆角和列表间距。Flutter 的跨平台能力很强,但不同平台的渲染细节仍然值得验证。
五、Todo 数据模型设计
5.1 Todo 类源码
当前项目定义了一个简洁的 Todo 数据模型。
dart
class Todo {
String title;
bool isCompleted;
DateTime createdAt;
Todo({required this.title, this.isCompleted = false, DateTime? createdAt})
: createdAt = createdAt ?? DateTime.now();
}
相比直接使用 List<String>,单独定义 Todo 类更利于扩展。后续如果要增加优先级、分类、提醒时间、截止日期、备注等字段,都可以在模型层继续演进。
5.2 字段说明
| 字段 | 类型 | 当前作用 | 可扩展方向 |
|---|---|---|---|
title |
String |
任务标题 | 可增加最大长度限制 |
isCompleted |
bool |
是否完成 | 可扩展完成时间 |
createdAt |
DateTime |
创建时间 | 可用于排序和归档 |
5.3 构造函数细节
dart
Todo({required this.title, this.isCompleted = false, DateTime? createdAt})
: createdAt = createdAt ?? DateTime.now();
这里使用了 Dart 的初始化列表。外部不传 createdAt 时,会自动使用 DateTime.now()。这让每条任务在创建时天然拥有时间戳,为后续按创建时间排序、展示日期分组或导出记录留下空间。
六、页面状态设计
6.1 StatefulWidget 的选择
TodoListHomePage 使用 StatefulWidget,因为任务列表会随着用户操作不断变化。
dart
class TodoListHomePage extends StatefulWidget {
const TodoListHomePage({super.key, required this.title});
final String title;
@override
State<TodoListHomePage> createState() => _TodoListHomePageState();
}
如果页面只是展示固定内容,可以使用 StatelessWidget;但 Todo List 需要新增、勾选、删除,所以必须具备可变状态。
6.2 核心状态字段
dart
class _TodoListHomePageState extends State<TodoListHomePage> {
final TextEditingController _controller = TextEditingController();
final TextEditingController _dialogController = TextEditingController();
final List<Todo> _todos = [];
}
三个字段职责清晰:
_controller:管理顶部输入框。_dialogController:管理弹窗输入框。_todos:保存当前页面内的所有任务。
6.3 生命周期释放
当前项目已经正确释放了两个控制器。
dart
@override
void dispose() {
_controller.dispose();
_dialogController.dispose();
super.dispose();
}
TextEditingController 持有监听和文本状态,组件销毁时释放它是一个好习惯。对于教学项目来说,这个细节非常值得在文章里写出来,因为它能体现代码不是"只跑起来就行",而是关注生命周期完整性。
七、新增任务逻辑拆解
7.1 顶部输入框提交
当前项目使用 _addTodoFromInput() 处理顶部输入框和按钮提交。
dart
void _addTodoFromInput() {
final title = _controller.text.trim();
if (title.isEmpty) {
_showAddTodoDialog();
return;
}
_addTodo(title);
_controller.clear();
}
这里的逻辑有两个亮点:
- 使用
trim()去掉首尾空格,避免添加"看起来为空"的任务。 - 当顶部输入为空时,不是静默失败,而是打开新增弹窗,引导用户继续输入。
7.2 统一添加方法
真正写入列表的逻辑封装在 _addTodo() 中。
dart
void _addTodo(String title) {
final trimmedTitle = title.trim();
if (trimmedTitle.isEmpty) {
return;
}
setState(() {
_todos.add(Todo(title: trimmedTitle));
});
}
这种设计避免了顶部输入框和弹窗各写一套新增逻辑。无论任务来自哪里,最终都走 _addTodo(),规则一致,维护成本也更低。
7.3 setState 的作用
setState 的核心作用是告诉 Flutter:当前 State 中的数据变了,需要重新执行 build(),让 UI 和数据保持一致。
dart
setState(() {
_todos.add(Todo(title: trimmedTitle));
});
如果忘记调用 setState,_todos 虽然已经添加了新数据,但界面不会立即刷新。这是 Flutter 入门阶段非常常见的问题。
八、弹窗新增任务交互
8.1 打开弹窗
当用户点击悬浮按钮,或顶部输入为空时点击添加按钮,应用会调用 _showAddTodoDialog()。

dart
Future<void> _showAddTodoDialog() async {
_dialogController.clear();
await showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Add task'),
content: TextField(
controller: _dialogController,
autofocus: true,
decoration: const InputDecoration(
hintText: 'What needs to be done?',
),
textInputAction: TextInputAction.done,
onSubmitted: (_) => _submitDialogTodo(context),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => _submitDialogTodo(context),
child: const Text('Add'),
),
],
);
},
);
}
这里使用 autofocus: true,弹窗打开后可以直接输入,体验比用户再手动点击输入框更顺滑。
8.2 提交弹窗任务
弹窗内的提交逻辑如下:
dart
void _submitDialogTodo(BuildContext dialogContext) {
final title = _dialogController.text.trim();
if (title.isEmpty) {
return;
}
_addTodo(title);
Navigator.of(dialogContext).pop();
}
这段代码先校验输入,再复用 _addTodo(),最后关闭弹窗。这样写可以保证新增规则只有一个入口,弹窗只是输入渠道之一。
8.3 两种新增入口对比
| 新增入口 | 触发方式 | 适合场景 | 复用方法 |
|---|---|---|---|
| 顶部输入框 | 输入后点击加号或键盘完成键 | 快速连续添加任务 | _addTodoFromInput() |
| 弹窗 | 点击 FAB 或空输入时触发 | 聚焦输入单条任务 | _showAddTodoDialog() |
| 统一写入 | 顶部和弹窗都调用 | 保持新增规则一致 | _addTodo() |
九、任务完成状态切换
9.1 toggle 方法
勾选任务时会调用 _toggleTodo(index)。
dart
void _toggleTodo(int index) {
setState(() {
_todos[index].isCompleted = !_todos[index].isCompleted;
});
}
这段代码使用布尔值取反,完成和未完成之间来回切换。由于任务保存在 _todos 列表中,index 就是当前列表项的位置。
9.2 Checkbox 绑定
dart
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => _toggleTodo(index),
),
Checkbox.value 读取当前任务状态,onChanged 触发状态更新。这里没有直接使用 onChanged 传入的值,而是调用 _toggleTodo(index) 做取反,逻辑简单直接。
9.3 完成后的视觉反馈
dart
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
color: todo.isCompleted ? Colors.grey : null,
),
),
当任务完成后,文本会显示删除线并变成灰色。这样的反馈非常重要,因为用户不需要阅读额外文案,就能看出任务状态。
十、删除任务逻辑拆解
10.1 delete 方法
删除任务逻辑如下:
dart
void _deleteTodo(int index) {
setState(() {
_todos.removeAt(index);
});
}
removeAt(index) 会从列表中移除指定位置的任务。配合 setState 后,列表会立即重新渲染。
10.2 删除按钮
dart
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _deleteTodo(index),
),
删除按钮放在 ListTile.trailing,符合用户对列表项操作的常见预期。红色图标也能提示这是一个破坏性操作。
10.3 可继续优化的交互
当前实现点击后会直接删除。对于正式产品,可以继续增加以下能力:
- 删除前弹出确认框。
- 删除后显示
SnackBar,支持撤销。 - 使用
Dismissible支持左滑删除。 - 删除已完成任务时批量清理。
示例优化代码如下:
dart
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Deleted: ${todo.title}'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
setState(() {
_todos.insert(index, todo);
});
},
),
),
);
十一、列表渲染与空状态设计
11.1 空列表提示
当 _todos 为空时,页面展示居中的提示文本。
dart
_todos.isEmpty
? const Center(
child: Text(
'No tasks yet. Add one above!',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
)
: ListView.builder(...)
空状态不是可有可无的细节。它能告诉用户当前页面没有数据,也能引导用户从顶部输入框开始添加任务。
11.2 ListView.builder
任务列表使用 ListView.builder 动态渲染。
dart
ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
final todo = _todos[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => _toggleTodo(index),
),
title: Text(todo.title),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _deleteTodo(index),
),
),
);
},
)
ListView.builder 适合渲染长度变化的列表。它按需构建列表项,在任务数量增加时比一次性生成大量 Widget 更合理。
11.3 页面结构图

这张结构图可以帮助读者快速理解页面层级:顶部输入区负责创建任务,中间区域负责展示列表,右下角 FAB 提供补充入口。
十二、OpenHarmony 适配关注点
12.1 Flutter 侧和平台侧的边界
当前项目主要业务逻辑都在 Flutter 层完成,OpenHarmony 侧负责承载应用入口、资源和模块配置。因此适配重点不是重写业务,而是确认 Flutter 页面在 OpenHarmony 容器中稳定启动。
text
Flutter 层:main.dart、Widget、状态、业务交互
OpenHarmony 层:Ability、模块配置、资源、构建和签名
12.2 平台检查清单
| 检查项 | 为什么重要 | 建议操作 |
|---|---|---|
| 应用名称 | 影响桌面展示和发布识别 | 检查 string.json |
| 应用图标 | 影响安装后识别度 | 检查 app_icon.png 和 icon.png |
| EntryAbility | 影响启动链路 | 检查 EntryAbility.ets |
| module 配置 | 影响页面和权限声明 | 检查 module.json5 |
| 插件注册 | 影响插件初始化 | 检查 GeneratedPluginRegistrant.ets |
| 签名配置 | 影响真机安装 | 在 DevEco Studio 中配置 |
12.3 当前项目的权限特点
Todo List 当前只在内存中保存任务,没有访问网络、定位、相机、传感器或本地文件,因此一般不需要额外权限。对于 OpenHarmony 适配来说,这是一个非常适合入门验证的项目。
如果后续加入持久化或提醒功能,可能会涉及以下方向:
- 本地数据库或文件读写。
- 后台提醒或通知。
- 跨设备同步。
- 账号体系和云端接口。
十三、测试与验证建议
13.1 手工验证流程
发布文章前,建议至少完成以下手工验证:
- 启动应用,确认首页能正常显示。
- 在顶部输入框输入
Buy milk,点击加号,确认任务出现在列表中。 - 输入只包含空格的内容,确认不会添加空白任务。
- 点击 FAB,确认弹窗出现并自动聚焦输入框。
- 在弹窗中输入任务并点击 Add,确认弹窗关闭且任务添加成功。
- 勾选任务,确认文本出现删除线并变灰。
- 点击删除图标,确认任务从列表移除。
- 删除所有任务后,确认空状态提示恢复显示。
13.2 Widget 测试思路
可以在 test/widget_test.dart 中补充基础交互测试。例如验证输入任务后列表出现对应文本:
dart
testWidgets('adds a todo from input', (WidgetTester tester) async {
await tester.pumpWidget(const TodoListApp());
await tester.enterText(find.byType(TextField).first, 'Read Flutter docs');
await tester.tap(find.byIcon(Icons.add).first);
await tester.pump();
expect(find.text('Read Flutter docs'), findsOneWidget);
});
如果要测试删除,可以先添加任务,再点击删除图标:
dart
testWidgets('deletes a todo item', (WidgetTester tester) async {
await tester.pumpWidget(const TodoListApp());
await tester.enterText(find.byType(TextField).first, 'Temporary task');
await tester.tap(find.byIcon(Icons.add).first);
await tester.pump();
await tester.tap(find.byIcon(Icons.delete));
await tester.pump();
expect(find.text('Temporary task'), findsNothing);
});
13.3 常用命令
| 命令 | 用途 | 建议频率 |
|---|---|---|
flutter pub get |
获取依赖 | 修改 pubspec.yaml 后执行 |
flutter analyze |
静态分析 | 每次提交前执行 |
flutter test |
运行测试 | 每次改动逻辑后执行 |
flutter run |
启动调试 | 功能开发期间执行 |
flutter build hap |
构建 OpenHarmony 包 | 发布或验收前执行 |
十四、常见问题与优化建议
14.1 为什么不直接用 List
List<String> 可以完成最简单的任务展示,但无法自然表达完成状态、创建时间等信息。使用 Todo 类后,任务数据更完整,也更适合后续扩展。
14.2 为什么要拆 _addTodo()
因为当前项目有两个新增入口:顶部输入框和弹窗。把真正添加逻辑放到 _addTodo(),可以避免重复代码,也能保证校验规则一致。
14.3 为什么要释放 Controller
TextEditingController 是带生命周期的对象。页面销毁时调用 dispose(),可以避免资源残留。当前项目已经处理了这一点,是一个值得保留的工程细节。
14.4 后续可以怎样升级
如果要把这个项目继续做成更完整的应用,可以优先考虑:
- 增加本地持久化,让任务重启后不丢失。
- 增加任务编辑能力。
- 增加任务优先级和分类。
- 增加已完成任务筛选。
- 增加撤销删除。
- 增加 Widget 测试和截图文档。
示例模型可以升级为:
dart
class Todo {
final String id;
String title;
bool isCompleted;
DateTime createdAt;
DateTime? completedAt;
int priority;
Todo({
required this.id,
required this.title,
this.isCompleted = false,
required this.createdAt,
this.completedAt,
this.priority = 0,
});
}
总结
todo_list 是一个非常适合讲解 Flutter + OpenHarmony 入门适配的项目。它的代码不复杂,但覆盖了输入、状态、列表、弹窗、样式反馈、资源释放和平台工程结构等关键知识点。
从工程角度看,这个项目有三个值得学习的地方:
- 使用
Todo模型表达任务,而不是只用字符串列表。 - 将新增逻辑统一到
_addTodo(),让顶部输入框和弹窗复用同一套规则。 - 在
dispose()中释放控制器,保留了良好的生命周期意识。
后续如果要继续完善,可以从本地持久化、编辑任务、筛选任务、撤销删除和自动化测试几个方向展开。这样一来,这个小项目不仅能作为 Flutter 入门 Demo,也能逐步演进成一个完整的跨平台工具应用。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注。你的支持是我持续创作 Flutter 与 OpenHarmony 实战内容的动力!
相关资源
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
- Flutter 官方文档:https://docs.flutter.dev/
- Dart 官方文档:https://dart.dev/guides
- Flutter Widget 目录:https://docs.flutter.dev/ui/widgets
- Material 3 设计:https://m3.material.io/
- pub.dev 包平台:https://pub.dev/
- OpenHarmony 官网:https://www.openharmony.cn/