Flutter for OpenHarmony轻量级开源记事本app实战:文件夹列表
本文介绍了一个基于Flutter的文件夹管理系统实现方案。系统采用GetX状态管理,构建了层级清晰的文件夹列表页面,支持创建、删除和查看功能。通过响应式设计自动更新UI,使用Card和ExpansionTile组件展示文件夹层级关系,并提供笔记数量统计。空状态处理提升了用户体验,操作按钮设计符合用户直觉。整体实现注重性能优化和视觉一致性,为笔记应用提供了有效的组织管理工具。
设计理念
文件夹是组织笔记的重要方式,就像电脑上的文件系统一样,可以帮助用户建立清晰的层级结构。一个好的文件夹管理系统不仅要支持创建和删除,还要能够展示层级关系,让用户一目了然。本文将详细介绍如何实现一个功能完善的文件夹列表页面。
页面基础结构
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../controllers/note_controller.dart';
import '../../models/category.dart';
import '../notes/widgets/empty_state.dart';
import 'folder_detail_page.dart';
class FolderPage extends StatelessWidget {
const FolderPage({super.key});
这段代码导入了页面所需的核心依赖。Material提供基础UI组件,GetX负责状态管理和路由导航,ScreenUtil确保在不同屏幕尺寸下的适配效果。NoteController是数据管理的核心,通过依赖注入的方式获取实例。EmptyState组件用于展示空列表状态,提升用户体验。FolderPage采用StatelessWidget设计,所有状态变化都由GetX的响应式系统处理。
构建页面框架
Widget build(BuildContext context) {
final controller = Get.find<NoteController>();
return Scaffold(
appBar: AppBar(
title: const Text('文件夹'),
actions: [
IconButton(
icon: const Icon(Icons.create_new_folder),
onPressed: () => _showCreateFolderDialog(context, controller),
build方法首先通过Get.find获取NoteController实例,这是GetX依赖注入的标准用法。Scaffold提供了页面的基本结构,包含AppBar和body两部分。AppBar展示"文件夹"标题,右侧actions区域放置创建按钮,使用create_new_folder图标直观表达功能。点击按钮触发_showCreateFolderDialog方法,弹出创建对话框。这种设计将操作入口集中在顶部,符合用户的操作习惯。
响应式列表渲染
),
],
),
body: Obx(() {
final rootFolders = controller.folders
.where((f) => f.parentId == null)
.toList();
if (rootFolders.isEmpty) {
return const EmptyState(
icon: Icons.folder_outlined,
body部分使用Obx包裹,实现响应式UI更新。当controller.folders数据变化时,页面会自动重新渲染。通过where过滤出parentId为null的根级文件夹,避免将子文件夹误显示在顶层。isEmpty判断用于处理空列表场景,此时返回EmptyState组件。EmptyState使用folder_outlined图标,视觉上与文件夹主题保持一致。这种设计确保用户在任何数据状态下都能获得清晰的界面反馈。
空状态处理
title: '暂无文件夹',
subtitle: '点击右上角创建文件夹',
);
}
return ListView.builder(
padding: EdgeInsets.all(12.w),
itemCount: rootFolders.length,
itemBuilder: (context, index) {
final folder = rootFolders[index];
final subFolders = controller.folders
EmptyState的title和subtitle为用户提供明确的操作指引,降低学习成本。当有数据时,使用ListView.builder构建列表,这是Flutter推荐的高性能列表渲染方案。padding使用ScreenUtil的w单位,确保在不同屏幕密度下保持一致的视觉效果。itemCount设置为rootFolders.length,确保只渲染必要的条目。在itemBuilder中,先获取当前folder对象,然后筛选出该文件夹的子文件夹。这种按需计算的方式避免了不必要的性能开销。
计算文件夹统计信息
.where((f) => f.parentId == folder.id)
.toList();
final noteCount = controller
.getNotesByFolder(folder.id)
.length;
return Card(
margin: EdgeInsets.only(bottom: 8.h),
child: ExpansionTile(
通过where过滤出parentId等于当前folder.id的子文件夹,建立父子关系。调用controller.getNotesByFolder获取该文件夹下的笔记列表,通过length属性得到笔记数量。这些统计信息将在UI中展示,帮助用户了解文件夹的内容规模。Card组件为每个文件夹项提供独立的视觉容器,margin设置底部间距,使列表更加清晰。ExpansionTile是可展开的列表项,非常适合展示层级结构。
文件夹主体信息展示
leading: const Icon(Icons.folder, color: Color(0xFF2196F3)),
title: Text(folder.name),
subtitle: Text('$noteCount 篇笔记'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.add, size: 20),
onPressed: () => _showCreateFolderDialog(
context, controller, parentId: folder.id),
leading使用蓝色文件夹图标,提供视觉识别点。title显示文件夹名称,subtitle展示笔记数量统计,让用户快速了解内容规模。trailing区域使用Row布局,放置添加和删除两个操作按钮。mainAxisSize.min确保Row只占用必要的宽度。添加按钮点击后调用_showCreateFolderDialog,并传入parentId参数,表示要在当前文件夹下创建子文件夹。这种设计将层级创建的入口直接放在父文件夹上,操作路径更短。
文件夹操作按钮
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
onPressed: () => _confirmDelete(context, controller, folder),
),
],
),
onExpansionChanged: (expanded) {},
children: [
ListTile(
删除按钮使用delete_outline图标,视觉上比实心图标更轻量,避免过于突出危险操作。点击触发_confirmDelete方法,弹出二次确认对话框,防止误删。onExpansionChanged回调在展开/收起时触发,这里暂时为空实现,预留扩展空间。children数组定义展开后显示的内容,第一项是"查看笔记"入口。这种设计将文件夹的详情查看放在展开区域,保持主列表的简洁性。
展开区域内容
leading: const Icon(Icons.arrow_forward),
title: const Text('查看笔记'),
onTap: () => Get.to(() => FolderDetailPage(folder: folder)),
),
...subFolders.map((sf) => ListTile(
leading: const Icon(Icons.folder_open, size: 20),
title: Text(sf.name),
subtitle: Text('${controller.getNotesByFolder(sf.id).length} 篇笔记'),
onTap: () => Get.to(() => FolderDetailPage(folder: sf)),
"查看笔记"ListTile使用arrow_forward图标,暗示点击后会进入新页面。点击后通过Get.to导航到FolderDetailPage,传入当前folder对象。展开运算符…将subFolders数组展开,为每个子文件夹生成一个ListTile。子文件夹使用folder_open图标,与父文件夹的folder图标形成视觉区分。subtitle同样显示笔记数量,保持信息展示的一致性。点击子文件夹也会进入详情页,实现层级导航。
子文件夹删除操作
trailing: IconButton(
icon: const Icon(Icons.delete_outline, size: 18),
onPressed: () => _confirmDelete(context, controller, sf),
),
)),
],
),
);
},
);
}),
);
}
子文件夹的trailing区域也放置删除按钮,图标尺寸略小(18),视觉上与层级关系匹配。删除操作同样调用_confirmDelete方法,传入子文件夹对象sf。map方法的结果通过展开运算符添加到children数组中,形成完整的展开内容。多层闭合括号结束ExpansionTile、Card、itemBuilder、ListView.builder、Obx和Scaffold的定义。这种层级结构清晰地展示了父子文件夹关系,用户可以直观地理解和操作。
创建文件夹对话框
void _showCreateFolderDialog(
BuildContext context,
NoteController controller,
{String? parentId}) {
final nameController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(parentId == null ? '新建文件夹' : '新建子文件夹'),
content: TextField(
_showCreateFolderDialog方法接收可选的parentId参数,用于区分创建根文件夹还是子文件夹。TextEditingController用于管理输入框的文本内容,这是Flutter表单处理的标准方式。showDialog弹出模态对话框,builder返回AlertDialog组件。title根据parentId是否为null动态显示不同文本,让用户明确当前操作的上下文。content区域放置TextField输入框,用于输入文件夹名称。这种设计通过一个方法复用了两种创建场景的逻辑。
输入框配置
controller: nameController,
decoration: const InputDecoration(
labelText: '文件夹名称',
border: OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextField绑定nameController,实现双向数据绑定。InputDecoration配置输入框的外观,labelText提供占位提示,border使用OutlineInputBorder样式,提供清晰的输入边界。actions数组定义对话框底部的操作按钮,第一个是取消按钮。TextButton是Material Design推荐的文本按钮样式,点击后调用Navigator.pop关闭对话框。这种设计为用户提供了明确的退出路径,避免误操作。
创建确认逻辑
ElevatedButton(
onPressed: () {
if (nameController.text.isNotEmpty) {
controller.createFolder(
nameController.text,
parentId: parentId);
Navigator.pop(context);
}
},
child: const Text('创建'),
),
],
),
);
}
创建按钮使用ElevatedButton,视觉上比TextButton更突出,引导用户完成主要操作。onPressed回调中先检查输入是否为空,避免创建无效文件夹。验证通过后调用controller.createFolder方法,传入名称和parentId参数。controller负责数据持久化和状态更新,页面层保持轻量。创建成功后立即关闭对话框,用户会看到列表自动更新。这种即时反馈提升了操作的流畅性。
删除确认对话框
void _confirmDelete(
BuildContext context,
NoteController controller,
Folder folder) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('删除文件夹'),
content: Text('确定要删除"${folder.name}"吗?文件夹内的笔记将移至未分类。'),
actions: [
_confirmDelete方法实现删除前的二次确认,这是危险操作的标准交互模式。AlertDialog的title明确操作类型,content详细说明删除后果。特别提示"笔记将移至未分类",让用户了解数据不会丢失,只是改变了组织方式。这种透明的信息披露降低了用户的操作焦虑。将folder.name嵌入提示文本中,帮助用户确认操作对象,避免删错。
删除操作执行
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red),
onPressed: () {
controller.deleteFolder(folder.id);
Navigator.pop(context);
},
child: const Text('删除'),
),
],
),
);
}
}
取消按钮提供安全退出路径,点击后仅关闭对话框,不执行任何操作。删除按钮使用红色背景,这是危险操作的通用视觉语言,提醒用户谨慎操作。点击后调用controller.deleteFolder方法,传入folder.id进行精确删除。删除操作由controller处理,包括数据库更新、笔记重新分类等复杂逻辑。操作完成后关闭对话框,Obx会自动触发列表刷新,用户立即看到结果。这种设计将业务逻辑与UI层完全分离,提高了代码的可维护性。
总结
文件夹列表页面是笔记应用的核心功能之一,通过ExpansionTile实现了层级展示,用户可以清晰地看到父子文件夹关系。创建和删除操作都提供了友好的对话框交互,确保操作的准确性和安全性。GetX的响应式系统让数据变化自动反映到UI上,无需手动管理状态更新。ScreenUtil确保了在不同设备上的一致体验。整体设计遵循Material Design规范,提供了直观、高效的文件夹管理体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)