# 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 实战内容的动力!


相关资源

相关推荐
lqj_本人7 小时前
Flutter_Rust_Bridge:让 Flutter 稳定调用 Rust 的跨语言桥梁
harmonyos
贾伟康7 小时前
【HarmonyOS 7新能力|040】QUIC长连接工程封装:把接入逻辑放进可维护的分层结构
网络编程·harmonyos·arkts·quic·软件架构
hqzing7 小时前
Harmonybrew 仓库中的 gcc(GCC 16)和 llvm(LLVM 23)已经可用
harmonyos
用户2181697049309 小时前
Flutter ClipPath
flutter
用户21816970493011 小时前
Flutter 折叠组件 ExpansionTile ExpansionPanelList
flutter
万物智能信息科技13 小时前
PWM散热风扇设置—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
人工智能·华为·开源·harmonyos·鸿蒙
aqi0015 小时前
一文读懂 HarmonyOS 7.0 带来的十大API重要升级
android·华为·harmonyos·鸿蒙·harmony
贾伟康16 小时前
【HarmonyOS 7新能力|037】数字盾工程封装:把接入逻辑放进可维护的分层结构
安全·harmonyos·arkts·软件架构·数字签名