# Flutter+OpenHarmony 实战:ToDo待办清单

前言

欢迎加入开源鸿蒙跨平台社区: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 技术文章、项目文档或作品集材料的读者。

本文重点回答三个问题:

  1. Flutter 待办清单应用如何组织数据模型、状态和 UI。
  2. TextEditingControllerListView.builderCheckboxAlertDialog 如何协同完成交互。
  3. 将 Flutter 项目放到 OpenHarmony 工程中时,发布前应该检查哪些配置和验证项。

效果图如下:


一、项目定位与功能概览

1.1 项目目标

todo_list 的目标很清晰:让用户输入一条任务,添加到列表中,然后可以勾选完成或删除任务。它虽然是一个入门级应用,但覆盖了 Flutter 开发中非常关键的几类能力:

  • 输入处理 :读取用户在 TextField 中输入的文本。
  • 状态管理 :使用 StatefulWidgetsetState 刷新页面。
  • 列表渲染 :通过 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 是经典案例,但它并不只是"增删改查"的玩具项目。一个任务从输入框进入应用,会经历以下完整链路:

  1. 用户输入文本。
  2. 控制器读取并 trim()
  3. 生成 Todo 数据对象。
  4. 将对象加入 _todos 列表。
  5. setState 通知 Flutter 重建 UI。
  6. ListView.builder 根据最新列表渲染任务卡片。
  7. 用户通过复选框或删除按钮继续改变状态。

提示:写这类项目文章时,不要只说"实现了增删改查",而要把 数据如何流动状态如何刷新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

TodoListAppStatelessWidget,因为它只负责应用级配置,不直接保存任务列表。

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'),
    );
  }
}

这里有三个重点:

  1. MaterialApp 提供 Material 应用基础能力。
  2. ColorScheme.fromSeed(seedColor: Colors.green) 使用绿色生成主题色。
  3. home 指向 TodoListHomePage,真正的业务状态从首页开始。

4.3 Material 3 风格

项目启用了 useMaterial3: true。这会影响 TextFieldElevatedButtonCardCheckboxFloatingActionButton 等组件的默认视觉风格。

注意:如果你的目标是多端一致体验,建议在真机上分别检查字体、按钮高度、输入框圆角和列表间距。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();
}

这里的逻辑有两个亮点:

  1. 使用 trim() 去掉首尾空格,避免添加"看起来为空"的任务。
  2. 当顶部输入为空时,不是静默失败,而是打开新增弹窗,引导用户继续输入。

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.pngicon.png
EntryAbility 影响启动链路 检查 EntryAbility.ets
module 配置 影响页面和权限声明 检查 module.json5
插件注册 影响插件初始化 检查 GeneratedPluginRegistrant.ets
签名配置 影响真机安装 在 DevEco Studio 中配置

12.3 当前项目的权限特点

Todo List 当前只在内存中保存任务,没有访问网络、定位、相机、传感器或本地文件,因此一般不需要额外权限。对于 OpenHarmony 适配来说,这是一个非常适合入门验证的项目。

如果后续加入持久化或提醒功能,可能会涉及以下方向:

  • 本地数据库或文件读写。
  • 后台提醒或通知。
  • 跨设备同步。
  • 账号体系和云端接口。

十三、测试与验证建议

13.1 手工验证流程

发布文章前,建议至少完成以下手工验证:

  1. 启动应用,确认首页能正常显示。
  2. 在顶部输入框输入 Buy milk,点击加号,确认任务出现在列表中。
  3. 输入只包含空格的内容,确认不会添加空白任务。
  4. 点击 FAB,确认弹窗出现并自动聚焦输入框。
  5. 在弹窗中输入任务并点击 Add,确认弹窗关闭且任务添加成功。
  6. 勾选任务,确认文本出现删除线并变灰。
  7. 点击删除图标,确认任务从列表移除。
  8. 删除所有任务后,确认空状态提示恢复显示。

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 入门适配的项目。它的代码不复杂,但覆盖了输入、状态、列表、弹窗、样式反馈、资源释放和平台工程结构等关键知识点。

从工程角度看,这个项目有三个值得学习的地方:

  1. 使用 Todo 模型表达任务,而不是只用字符串列表。
  2. 将新增逻辑统一到 _addTodo(),让顶部输入框和弹窗复用同一套规则。
  3. dispose() 中释放控制器,保留了良好的生命周期意识。

后续如果要继续完善,可以从本地持久化、编辑任务、筛选任务、撤销删除和自动化测试几个方向展开。这样一来,这个小项目不仅能作为 Flutter 入门 Demo,也能逐步演进成一个完整的跨平台工具应用。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注。你的支持是我持续创作 Flutter 与 OpenHarmony 实战内容的动力!


相关资源

相关推荐
不羁的木木2 小时前
Form Kit(卡片开发服务)学习笔记04-交互事件与跳转处理
笔记·学习·交互·harmonyos
不爱吃糖的程序媛10 小时前
Flutter 三方库适配鸿蒙教程
flutter·华为·harmonyos
不羁的木木10 小时前
HarmonyOS文件基础服务(Core File Kit)实战演练04-文件监听与流式读写
华为·harmonyos
不羁的木木11 小时前
ArkWeb实战学习笔记05-综合实战:构建混合应用
笔记·学习·harmonyos
芒鸽13 小时前
鸿蒙应用测试实战:从单元测试到自动化测试
华为·单元测试·harmonyos
Davina_yu13 小时前
Hello HarmonyOS:搭建DevEco Studio开发环境与第一个应用运行(1)
harmonyos·鸿蒙原生开发
2501_9197490313 小时前
鸿蒙 Flutter 实战:video_compress 3.1.4 适配 3.27-ohos 全流程
flutter·华为·harmonyos
nashane14 小时前
HarmonyOS 6学习:应用退出动画优化实战——从“闪退“到优雅退出的完美蜕变
学习·华为·harmonyos
h64648564h15 小时前
Flutter 国际化(i18n)全指南:一键切换中/英/日多语言
前端·javascript·flutter