设计理念

文件夹是组织笔记的重要方式,用户可以将相关的笔记放在同一个文件夹中,方便管理和查找。文件夹详情页面需要展示文件夹中的所有笔记,并提供创建新笔记的功能。本文将详细介绍如何实现一个功能完整的文件夹详情页面。
请添加图片描述

导入依赖包

首先导入页面所需的核心依赖包。

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/note_card.dart';
import '../notes/widgets/empty_state.dart';
import '../editor/note_editor_page.dart';

这里导入了Flutter的Material组件库、GetX状态管理库和屏幕适配库。同时引入了笔记控制器、文件夹模型以及笔记卡片、空状态和编辑器等自定义组件。这些依赖为文件夹详情页面提供了完整的功能支持,包括UI渲染、状态管理、响应式布局和业务逻辑处理。

页面类定义

定义文件夹详情页面的基础结构。

class FolderDetailPage extends StatelessWidget {
  final Folder folder;

  const FolderDetailPage({super.key, required this.folder});

  
  Widget build(BuildContext context) {
    final controller = Get.find<NoteController>();

FolderDetailPage继承自StatelessWidget,因为页面的状态由GetX管理,不需要内部状态。通过构造函数接收Folder对象,这是必需参数,确保页面始终有文件夹数据。在build方法中通过Get.find获取NoteController实例,这是GetX的依赖注入机制,保证全局只有一个控制器实例。这种设计分离了UI和业务逻辑,使代码更易维护。

构建Scaffold结构

构建页面的基本框架。

    return Scaffold(
      appBar: AppBar(
        title: Text(folder.name),
      ),
      body: Obx(() {
        final notes = controller.getNotesByFolder(folder.id);
        if (notes.isEmpty) {
          return const EmptyState(
            icon: Icons.folder_open,
            title: '暂无笔记',

Scaffold提供了Material Design的基本页面结构。AppBar显示文件夹名称作为标题,让用户清楚当前所在位置。body部分使用Obx包裹,这是GetX的响应式组件,当controller中的数据变化时会自动重建UI。通过getNotesByFolder方法获取当前文件夹下的所有笔记,如果为空则显示EmptyState组件。这种响应式设计确保了数据变化时UI能实时更新。

空状态显示

当文件夹中没有笔记时显示友好的空状态提示。

            subtitle: '该文件夹下还没有笔记',
          );
        }

EmptyState组件使用文件夹图标和提示文字,告知用户当前文件夹为空。这种设计避免了空白页面带来的困惑,给用户明确的反馈。subtitle提示用户可以通过右下角的按钮创建笔记,引导用户进行下一步操作。良好的空状态设计能提升用户体验,让应用显得更加专业和友好。

笔记列表构建

使用ListView.builder构建可滚动的笔记列表。

        return ListView.builder(
          padding: EdgeInsets.all(12.w),
          itemCount: notes.length,
          itemBuilder: (context, index) {
            final note = notes[index];
            return NoteCard(
              note: note,
              onTap: () => Get.to(() => NoteEditorPage(note: note)),

ListView.builder是Flutter中高效的列表构建方式,只渲染可见区域的item,适合大量数据的场景。padding设置为12.w使用了屏幕适配,确保在不同设备上有一致的视觉效果。itemBuilder回调中为每个笔记创建NoteCard组件,onTap回调使用GetX的路由导航跳转到编辑页面。这种懒加载机制保证了列表的流畅性能。

笔记卡片交互

为笔记卡片添加长按和滑动删除功能。

              onLongPress: () {},
              onDismissed: (direction) {
                if (direction == DismissDirection.endToStart) {
                  controller.deleteNote(note.id);
                } else {
                  controller.toggleFavorite(note.id);
                }
              },
            );
          },
        );

onLongPress预留了长按事件的接口,可用于未来扩展批量选择等功能。onDismissed实现了滑动操作,从右向左滑动(endToStart)删除笔记,从左向右滑动收藏笔记。这种手势交互符合用户的操作习惯,提供了快捷的笔记管理方式。通过direction判断滑动方向,调用不同的控制器方法完成相应操作。

浮动操作按钮

添加创建新笔记的浮动按钮。

      }),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final note = controller.createNote(folderId: folder.id);
          Get.to(() => NoteEditorPage(note: note));
        },
        child: const Icon(Icons.add, color: Colors.white),
      ),
    );
  }
}

FloatingActionButton是Material Design中的标准组件,通常用于页面的主要操作。点击按钮时调用createNote方法创建新笔记,传入folderId参数确保笔记归属于当前文件夹。创建完成后立即跳转到编辑页面,让用户可以无缝地开始编写内容。白色的加号图标在主题色背景上清晰可见,符合Material Design规范。

编辑文件夹对话框

实现编辑文件夹名称和颜色的对话框。

void _showEditDialog(BuildContext context, NoteController controller) {
  final nameController = TextEditingController(text: folder.name);
  String selectedColor = folder.color;
  
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('编辑文件夹'),
      content: Column(

_showEditDialog方法创建一个编辑对话框,使用TextEditingController预填充当前文件夹名称。selectedColor变量保存用户选择的颜色,初始值为文件夹当前颜色。AlertDialog是Flutter的标准对话框组件,title显示"编辑文件夹"标题。content使用Column垂直排列输入框和颜色选择器,mainAxisSize设为min让对话框高度自适应内容。

文件夹名称输入

添加文件夹名称的输入框。

        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: nameController,
            decoration: const InputDecoration(
              labelText: '文件夹名称',
              border: OutlineInputBorder(),
            ),
          ),
          SizedBox(height: 16.h),

TextField使用nameController管理输入内容,InputDecoration配置输入框的外观。labelText显示"文件夹名称"标签,border使用OutlineInputBorder提供边框样式。SizedBox添加16像素的垂直间距,使用.h后缀进行屏幕适配。这种设计让输入框清晰易用,边框样式符合Material Design规范。

颜色选择器集成

在对话框中集成颜色选择器组件。

          _buildColorPicker(selectedColor, (color) {
            selectedColor = color;
          }),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),

_buildColorPicker方法创建颜色选择器,传入当前选中的颜色和回调函数。当用户选择新颜色时,回调函数更新selectedColor变量。这种设计将颜色选择逻辑封装在独立方法中,保持代码的模块化。actions数组定义对话框底部的操作按钮,TextButton用于取消操作,点击后关闭对话框。

保存按钮逻辑

实现保存文件夹修改的逻辑。

          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: () {
            if (nameController.text.isNotEmpty) {
              controller.updateFolder(
                folder.id,
                nameController.text,
                selectedColor,
              );
              Navigator.pop(context);

ElevatedButton用于保存操作,具有更强的视觉权重。点击时首先验证名称不为空,这是必要的数据校验。验证通过后调用updateFolder方法更新文件夹信息,传入文件夹ID、新名称和新颜色。更新成功后关闭对话框返回列表页面。这种设计确保了数据的完整性和操作的流畅性。

颜色选择器实现

创建颜色选择器的UI组件。

Widget _buildColorPicker(String selectedColor, Function(String) onColorSelected) {
  final colors = [
    '#2196F3', '#4CAF50', '#FF9800', '#F44336',
    '#9C27B0', '#00BCD4', '#FFEB3B', '#795548',
  ];

  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      const Text('选择颜色'),

_buildColorPicker方法接收当前选中颜色和选择回调函数。colors数组定义了8种预设颜色,包括蓝色、绿色、橙色、红色、紫色、青色、黄色和棕色,覆盖了常用的色彩范围。Column使用crossAxisAlignment.start让内容左对齐。这些颜色值使用十六进制格式,便于在代码中使用和转换。

颜色网格布局

使用Wrap组件实现颜色选项的网格布局。

      SizedBox(height: 8.h),
      Wrap(
        spacing: 8.w,
        runSpacing: 8.h,
        children: colors.map((color) {
          final isSelected = color == selectedColor;
          return GestureDetector(
            onTap: () => onColorSelected(color),
            child: Container(

Wrap组件类似于Row,但当空间不足时会自动换行,非常适合展示颜色选项。spacing设置水平间距,runSpacing设置垂直间距,都使用屏幕适配。通过map遍历colors数组创建颜色选项,isSelected判断当前颜色是否被选中。GestureDetector包裹Container实现点击交互,点击时调用onColorSelected回调。

颜色选项样式

设计每个颜色选项的视觉样式。

              width: 32.w,
              height: 32.h,
              decoration: BoxDecoration(
                color: Color(int.parse(color.replaceFirst('#', '0xFF'))),
                borderRadius: BorderRadius.circular(16),
                border: isSelected
                    ? Border.all(color: Colors.black, width: 2)
                    : null,
              ),

Container设置为32x32的正方形,使用屏幕适配确保不同设备上大小一致。BoxDecoration定义容器的装饰效果,color通过解析十六进制字符串获得,将’#'替换为’0xFF’转换为Flutter的Color对象。borderRadius设为16创建圆形效果。选中状态显示2像素宽的黑色边框,未选中则无边框,这种视觉反馈让用户清楚当前选择。

选中状态标识

为选中的颜色添加勾选图标。

              child: isSelected
                  ? const Icon(Icons.check, color: Colors.white, size: 16)
                  : null,
            ),
          );
        }).toList(),
      ),
    ],
  );
}

Container的child根据isSelected条件显示内容。选中时显示白色的勾选图标,大小为16像素,在彩色背景上清晰可见。未选中时child为null,容器内部为空。这种双重视觉反馈(边框+图标)让选中状态更加明显。toList()将map结果转换为列表传给Wrap的children。

搜索功能页面定义

为文件夹添加搜索功能的增强版本。

class SearchableFolderDetail extends StatefulWidget {
  final Folder folder;

  const SearchableFolderDetail({super.key, required this.folder});

  
  State<SearchableFolderDetail> createState() => _SearchableFolderDetailState();
}

SearchableFolderDetail继承StatefulWidget,因为搜索功能需要管理搜索框的输入状态和过滤后的笔记列表。接收Folder参数用于获取文件夹信息。createState方法创建对应的State对象,这是Flutter状态管理的标准模式。相比StatelessWidget,StatefulWidget能够响应用户输入并动态更新UI。

搜索状态初始化

定义搜索页面的状态变量和初始化逻辑。

class _SearchableFolderDetailState extends State<SearchableFolderDetail> {
  final TextEditingController _searchController = TextEditingController();
  List<Note> _filteredNotes = [];

  
  void initState() {
    super.initState();
    _searchController.addListener(_filterNotes);
    _loadNotes();
  }

State类定义了两个关键变量:_searchController管理搜索框输入,_filteredNotes存储过滤后的笔记列表。initState是生命周期方法,在组件创建时调用。为searchController添加监听器,每次输入变化时自动调用_filterNotes方法。_loadNotes方法加载初始笔记列表。这种设计实现了实时搜索的效果。

资源清理

在组件销毁时清理资源。

  
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

dispose是生命周期方法,在组件销毁时调用。必须手动释放TextEditingController资源,避免内存泄漏。这是Flutter开发的重要实践,所有创建的控制器、动画、流等资源都应在dispose中释放。先调用_searchController.dispose(),再调用super.dispose(),这是标准的清理顺序。

加载笔记数据

从控制器获取文件夹内的笔记。

  void _loadNotes() {
    final controller = Get.find<NoteController>();
    final notes = controller.getNotesByFolder(widget.folder.id);
    setState(() {
      _filteredNotes = notes;
    });
  }

_loadNotes方法通过GetX获取NoteController实例,调用getNotesByFolder获取当前文件夹的所有笔记。使用setState更新_filteredNotes,触发UI重建。widget.folder访问StatefulWidget的属性,这是State类访问Widget属性的标准方式。初始状态下显示所有笔记,用户输入搜索词后才进行过滤。

搜索过滤逻辑

实现笔记的实时搜索过滤功能。

  void _filterNotes() {
    final controller = Get.find<NoteController>();
    final allNotes = controller.getNotesByFolder(widget.folder.id);
    final query = _searchController.text.toLowerCase();
    
    setState(() {
      _filteredNotes = allNotes.where((note) =>
        note.title.toLowerCase().contains(query) ||
        note.content.toLowerCase().contains(query)
      ).toList();
    });
  }

_filterNotes方法在每次搜索框内容变化时被调用。获取所有笔记后,将搜索词转为小写实现大小写不敏感搜索。使用where方法过滤笔记,检查标题或内容是否包含搜索词。contains方法进行模糊匹配,比精确匹配更友好。setState触发UI更新显示过滤结果。这种实时搜索提供了流畅的用户体验。

搜索页面结构

构建带搜索框的页面框架。

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.folder.name),
        bottom: PreferredSize(
          preferredSize: Size.fromHeight(60.h),
          child: Padding(
            padding: EdgeInsets.all(16.w),

Scaffold的appBar使用bottom属性添加搜索框,这是在AppBar下方添加自定义内容的标准方式。PreferredSize指定bottom区域的高度为60像素(适配屏幕)。Padding为搜索框添加16像素的内边距,确保搜索框不会紧贴屏幕边缘。这种布局让搜索框始终可见,方便用户随时搜索。

搜索输入框

创建搜索框的UI组件。

            child: TextField(
              controller: _searchController,
              decoration: InputDecoration(
                hintText: '搜索笔记',
                prefixIcon: const Icon(Icons.search),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(25),
                ),
                contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
              ),
            ),

TextField使用_searchController管理输入,InputDecoration配置搜索框外观。hintText显示占位提示文字,prefixIcon在左侧显示搜索图标。border使用圆角边框,borderRadius为25创建胶囊形状,这是搜索框的常见设计。contentPadding设置内边距,让文字和边框保持适当距离。这种设计符合用户对搜索框的视觉预期。

搜索结果显示

根据搜索结果显示不同的UI状态。

          ),
        ),
      ),
      body: _filteredNotes.isEmpty
          ? const EmptyState(
              icon: Icons.search_off,
              title: '未找到笔记',
              subtitle: '尝试使用其他关键词',
            )
          : ListView.builder(

body部分根据_filteredNotes是否为空显示不同内容。为空时显示EmptyState,使用search_off图标和提示文字,引导用户尝试其他搜索词。不为空时使用ListView.builder展示搜索结果。这种条件渲染让用户清楚搜索状态,避免空白页面带来的困惑。

搜索结果列表

构建搜索结果的笔记列表。

              padding: EdgeInsets.all(12.w),
              itemCount: _filteredNotes.length,
              itemBuilder: (context, index) {
                final note = _filteredNotes[index];
                return NoteCard(
                  note: note,
                  onTap: () => Get.to(() => NoteEditorPage(note: note)),
                  onLongPress: () {},

ListView.builder使用_filteredNotes作为数据源,显示过滤后的笔记。itemCount设为过滤列表的长度,确保只渲染匹配的笔记。NoteCard展示每条笔记,点击跳转到编辑页面。onLongPress预留了长按接口,可用于未来扩展批量选择功能。这种设计让搜索结果的交互与普通列表保持一致。

搜索结果交互

为搜索结果添加滑动操作。

                  onDismissed: (direction) {
                    final controller = Get.find<NoteController>();
                    if (direction == DismissDirection.endToStart) {
                      controller.deleteNote(note.id);
                    } else {
                      controller.toggleFavorite(note.id);
                    }
                  },
                );
              },
            ),
    );
  }
}

onDismissed回调处理滑动删除和收藏操作。从右向左滑动删除笔记,从左向右滑动切换收藏状态。在回调中获取NoteController实例,调用相应的方法。这种手势交互在搜索结果中同样可用,保持了操作的一致性。删除或收藏后,_filterNotes会自动更新列表显示。

批量操作页面定义

创建支持批量操作的文件夹详情页面。

class BatchFolderDetail extends StatefulWidget {
  final Folder folder;

  const BatchFolderDetail({super.key, required this.folder});

  
  State<BatchFolderDetail> createState() => _BatchFolderDetailState();
}

BatchFolderDetail继承StatefulWidget,用于管理批量选择的状态。接收Folder参数获取文件夹信息。批量操作需要跟踪哪些笔记被选中,以及当前是否处于选择模式,这些状态需要在State中管理。createState创建对应的State对象,这是实现复杂交互的标准模式。

批量操作状态管理

定义批量选择所需的状态变量。

class _BatchFolderDetailState extends State<BatchFolderDetail> {
  final Set<String> _selectedNotes = {};
  bool _isSelectionMode = false;

  void _toggleSelection(String noteId) {
    setState(() {
      if (_selectedNotes.contains(noteId)) {
        _selectedNotes.remove(noteId);
        if (_selectedNotes.isEmpty) {
          _isSelectionMode = false;

State类定义了两个关键变量:_selectedNotes使用Set存储选中笔记的ID,Set保证ID唯一性且查找效率高。_isSelectionMode标记是否处于选择模式。_toggleSelection方法切换笔记的选中状态,如果笔记已选中则移除,移除后如果集合为空则退出选择模式。这种设计让选择状态的管理清晰高效。

选择状态切换

完成选择状态的切换逻辑。

        }
      } else {
        _selectedNotes.add(noteId);
        _isSelectionMode = true;
      }
    });
  }

如果笔记未被选中,则添加到_selectedNotes集合并进入选择模式。setState触发UI更新,让选中状态立即反映在界面上。这种双向切换让用户可以灵活地选择和取消选择笔记。进入选择模式后,AppBar会显示选中数量和批量操作按钮,提供清晰的视觉反馈。

全选功能

实现一键选中所有笔记的功能。

  void _selectAll() {
    final controller = Get.find<NoteController>();
    final notes = controller.getNotesByFolder(widget.folder.id);
    setState(() {
      _selectedNotes.clear();
      _selectedNotes.addAll(notes.map((note) => note.id));
      _isSelectionMode = true;
    });
  }

_selectAll方法获取文件夹内所有笔记,清空当前选择后将所有笔记ID添加到_selectedNotes。使用map提取每个笔记的ID,addAll批量添加。设置_isSelectionMode为true确保处于选择模式。这个功能在需要批量操作所有笔记时非常有用,比如批量删除或移动。

清除选择

实现清除所有选择并退出选择模式。

  void _clearSelection() {
    setState(() {
      _selectedNotes.clear();
      _isSelectionMode = false;
    });
  }

_clearSelection方法清空_selectedNotes集合并退出选择模式。setState触发UI更新,让界面恢复到正常浏览模式。这个功能通常绑定到AppBar的取消按钮,让用户可以快速退出选择模式。清除选择后,所有笔记卡片的选中标记消失,AppBar恢复显示文件夹名称。

批量删除

实现批量删除选中笔记的功能。

  void _batchDelete() {
    final controller = Get.find<NoteController>();
    for (final noteId in _selectedNotes) {
      controller.deleteNote(noteId);
    }
    _clearSelection();
    Get.snackbar('成功', '已删除选中的笔记');
  }

_batchDelete方法遍历_selectedNotes集合,逐个调用deleteNote删除笔记。删除完成后调用_clearSelection退出选择模式。使用Get.snackbar显示成功提示,给用户明确的操作反馈。这种批量操作大大提高了管理大量笔记的效率,用户可以一次性删除多条笔记而不需要逐个操作。

批量移动预留

为批量移动功能预留接口。

  void _batchMove() {
    // 实现批量移动功能
  }

_batchMove方法预留了批量移动功能的接口。实现时可以弹出文件夹选择对话框,让用户选择目标文件夹,然后将选中的笔记移动过去。这个功能对于重新组织笔记非常有用,用户可以快速将多条笔记从一个文件夹移动到另一个文件夹。

批量操作AppBar

构建支持批量操作的AppBar。

  
  Widget build(BuildContext context) {
    final controller = Get.find<NoteController>();
    final notes = controller.getNotesByFolder(widget.folder.id);
    
    return Scaffold(
      appBar: AppBar(
        title: Text(_isSelectionMode 
            ? '已选择${_selectedNotes.length}项' 
            : widget.folder.name),

build方法首先获取NoteController和笔记列表。AppBar的title根据_isSelectionMode动态显示内容:选择模式下显示已选择的数量,正常模式下显示文件夹名称。这种动态标题让用户清楚当前状态和选择数量。使用字符串插值显示选中数量,提供实时反馈。

批量操作按钮

添加批量操作的工具栏按钮。

        actions: [
          if (_isSelectionMode) ...[
            IconButton(
              icon: const Icon(Icons.select_all),
              onPressed: _selectAll,
            ),
            IconButton(
              icon: const Icon(Icons.clear),
              onPressed: _clearSelection,
            ),
            IconButton(
              icon: const Icon(Icons.delete),

actions数组使用条件展开运算符,只在选择模式下显示批量操作按钮。select_all图标用于全选,clear图标用于清除选择,delete图标用于批量删除。每个IconButton绑定对应的方法。这种设计让工具栏根据模式动态变化,避免在正常模式下显示不相关的按钮。

批量移动按钮

添加批量移动功能的按钮。

              onPressed: _batchDelete,
            ),
            IconButton(
              icon: const Icon(Icons.drive_file_move),
              onPressed: _batchMove,
            ),
          ],
        ],
      ),

drive_file_move图标表示移动操作,点击调用_batchMove方法。所有批量操作按钮排列在AppBar右侧,形成一组工具栏。这些按钮只在选择模式下可见,保持界面的简洁性。用户可以根据需要选择不同的批量操作,提高了笔记管理的灵活性。

批量选择列表

构建支持批量选择的笔记列表。

      body: ListView.builder(
        padding: EdgeInsets.all(12.w),
        itemCount: notes.length,
        itemBuilder: (context, index) {
          final note = notes[index];
          final isSelected = _selectedNotes.contains(note.id);
          
          return NoteCard(
            note: note,
            isSelected: isSelected,

ListView.builder构建笔记列表,为每个笔记判断是否被选中。isSelected变量通过检查_selectedNotes是否包含笔记ID来确定。将isSelected传递给NoteCard,让卡片根据选中状态显示不同的样式,比如背景色变化或显示勾选标记。这种视觉反馈让用户清楚哪些笔记已被选中。

选择模式交互

配置选择模式下的笔记卡片交互。

            isSelectionMode: _isSelectionMode,
            onTap: () {
              if (_isSelectionMode) {
                _toggleSelection(note.id);
              } else {
                Get.to(() => NoteEditorPage(note: note));
              }
            },
            onLongPress: () => _toggleSelection(note.id),

isSelectionMode参数告诉NoteCard当前是否处于选择模式。onTap回调根据模式执行不同操作:选择模式下切换选中状态,正常模式下打开编辑页面。onLongPress始终触发选择,这是进入选择模式的快捷方式。这种设计让用户可以通过长按任意笔记快速进入批量操作模式。

批量模式滑动操作

在批量模式下保留滑动操作。

            onDismissed: (direction) {
              if (direction == DismissDirection.endToStart) {
                controller.deleteNote(note.id);
              } else {
                controller.toggleFavorite(note.id);
              }
            },
          );
        },
      ),

即使在批量选择模式下,滑动删除和收藏功能仍然可用。这给用户提供了更多操作选择,可以混合使用批量操作和单个操作。从右向左滑动删除笔记,从左向右滑动切换收藏状态。这种灵活性让用户可以根据具体情况选择最高效的操作方式。

批量模式创建按钮

在批量模式下保留创建笔记功能。

      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final note = controller.createNote(folderId: widget.folder.id);
          Get.to(() => NoteEditorPage(note: note));
        },
        child: const Icon(Icons.add, color: Colors.white),
      ),
    );
  }
}

FloatingActionButton在批量选择模式下仍然可用,让用户随时可以创建新笔记。点击按钮创建笔记并跳转到编辑页面,创建的笔记自动归属于当前文件夹。这种设计保持了功能的完整性,用户不需要退出选择模式就能创建笔记。白色加号图标在主题色背景上清晰可见。

统计信息卡片

创建显示文件夹统计信息的卡片组件。

Widget _buildStatisticsCard(NoteController controller) {
  final notes = controller.getNotesByFolder(folder.id);
  final totalNotes = notes.length;
  final favoriteNotes = notes.where((n) => n.isFavorite).length;
  final totalWords = notes.fold<int>(0, (sum, note) => sum + note.wordCount);
  final avgWords = totalNotes > 0 ? totalWords ~/ totalNotes : 0;

  return Card(
    child: Padding(

_buildStatisticsCard方法计算文件夹的各项统计数据。totalNotes是笔记总数,favoriteNotes通过where过滤收藏笔记并计数。totalWords使用fold累加所有笔记的字数,avgWords计算平均字数,使用整除运算符~/避免除零错误。这些统计数据让用户了解文件夹的使用情况和内容规模。

统计卡片布局

设计统计信息的布局结构。

      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '统计信息',
            style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
          ),
          SizedBox(height: 12.h),

Card组件提供卡片样式,Padding添加16像素内边距。Column垂直排列统计内容,crossAxisAlignment.start让内容左对齐。标题"统计信息"使用16号粗体字,SizedBox添加12像素间距。这种布局清晰地组织统计数据,标题和内容层次分明。

统计数据行

使用Row布局显示统计项。

          Row(
            children: [
              Expanded(
                child: _StatItem(
                  label: '笔记数量',
                  value: '$totalNotes',
                  icon: Icons.note,
                ),
              ),
              Expanded(
                child: _StatItem(
                  label: '收藏数量',

Row水平排列统计项,每个Expanded让子组件平分可用空间。_StatItem是自定义组件,接收标签、数值和图标参数。第一行显示笔记数量和收藏数量,使用note和star图标。Expanded确保两个统计项宽度相等,创建对称的布局效果。

第二行统计数据

添加字数相关的统计信息。

                  value: '$favoriteNotes',
                  icon: Icons.star,
                ),
              ),
            ],
          ),
          SizedBox(height: 12.h),
          Row(
            children: [
              Expanded(
                child: _StatItem(
                  label: '总字数',
                  value: '$totalWords',

第二行显示总字数和平均字数统计。SizedBox在两行之间添加12像素间距,保持视觉上的分隔。总字数使用text_fields图标,平均字数使用calculate图标。这些统计帮助用户了解笔记的内容量,对于写作目标和内容管理很有参考价值。

完成统计卡片

完成统计卡片的构建。

                  icon: Icons.text_fields,
                ),
              ),
              Expanded(
                child: _StatItem(
                  label: '平均字数',
                  value: '$avgWords',
                  icon: Icons.calculate,
                ),
              ),
            ],
          ),
        ],
      ),
    ),
  );
}

两行统计项形成2x2的网格布局,展示四个关键指标。所有统计项使用相同的_StatItem组件,保持视觉一致性。Card的阴影和圆角让统计卡片从背景中突出。这种紧凑的布局在有限空间内展示了丰富的信息,用户一眼就能了解文件夹的整体情况。

统计项组件定义

创建统计项的专用组件类。

class _StatItem extends StatelessWidget {
  final String label;
  final String value;
  final IconData icon;

  const _StatItem({
    required this.label,
    required this.value,
    required this.icon,
  });

_StatItem是私有的StatelessWidget,用于显示单个统计项。接收三个必需参数:label是统计项名称,value是统计数值,icon是图标。使用下划线前缀表示这是私有类,只在当前文件内使用。这种组件化设计提高了代码复用性,所有统计项使用统一的样式和布局。

统计项布局

构建统计项的视觉布局。

  
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(12.w),
      child: Column(
        children: [
          Icon(icon, size: 24.sp, color: const Color(0xFF2196F3)),
          SizedBox(height: 4.h),

Container添加12像素内边距,Column垂直排列图标、数值和标签。图标大小为24像素,使用蓝色主题色(#2196F3),这是Material Design的标准蓝色。SizedBox在图标和数值之间添加4像素间距。这种垂直布局让统计项清晰易读,图标在上方吸引注意力。

统计数值显示

显示统计项的数值。

          Text(
            value,
            style: TextStyle(
              fontSize: 18.sp,
              fontWeight: FontWeight.bold,
              color: const Color(0xFF2196F3),
            ),
          ),
          SizedBox(height: 2.h),

数值使用18号粗体字,同样使用蓝色主题色,与图标颜色一致。粗体和较大的字号让数值成为视觉焦点。SizedBox在数值和标签之间添加2像素间距,间距较小因为它们关系更紧密。这种层次化的字体大小和颜色使用让信息层次清晰。

统计标签显示

显示统计项的标签文字。

          Text(
            label,
            style: TextStyle(fontSize: 12.sp, color: Colors.grey),
          ),
        ],
      ),
    );
  }
}

标签使用12号灰色字体,字号较小且颜色较淡,作为辅助信息。这种设计让数值成为主要信息,标签提供说明。整个统计项从上到下依次是图标、数值、标签,形成清晰的信息层次。这种设计符合用户的视觉习惯,快速扫视就能获取关键信息。

主题适配页面定义

创建支持主题切换的文件夹详情页面。

class ThemedFolderDetail extends StatelessWidget {
  final Folder folder;

  const ThemedFolderDetail({super.key, required this.folder});

  
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final controller = Get.find<NoteController>();

ThemedFolderDetail继承StatelessWidget,专注于主题适配。通过Theme.of(context)获取当前主题配置,这是Flutter的主题系统核心API。获取NoteController用于数据操作。这种设计让页面能够响应系统或应用的主题变化,在深色和浅色模式下都有良好的显示效果。

主题化Scaffold

使用主题色配置Scaffold。

    return Scaffold(
      backgroundColor: theme.scaffoldBackgroundColor,
      appBar: AppBar(
        backgroundColor: theme.appBarTheme.backgroundColor,
        title: Text(
          folder.name,
          style: TextStyle(color: theme.appBarTheme.titleTextStyle?.color),
        ),
        iconTheme: IconThemeData(color: theme.appBarTheme.iconTheme?.color),

Scaffold的backgroundColor使用主题的脚手架背景色,确保与应用整体风格一致。AppBar的背景色、标题颜色和图标颜色都从主题中获取。使用可选链操作符?.安全访问嵌套属性,避免空指针异常。这种完全基于主题的配置让页面能够无缝适配不同的主题模式。

主题化笔记列表

构建适配主题的笔记列表。

      ),
      body: Obx(() {
        final notes = controller.getNotesByFolder(folder.id);
        if (notes.isEmpty) {
          return const EmptyState(
            icon: Icons.note_outlined,
            title: '文件夹为空',
            subtitle: '点击右下角创建第一条笔记',
          );
        }

body使用Obx包裹实现响应式更新。获取文件夹内的笔记,为空时显示EmptyState组件。EmptyState组件内部也应该适配主题,使用主题的文字颜色和图标颜色。这种设计确保了空状态在不同主题下都有良好的可读性,不会出现深色背景配浅色文字的问题。

主题化列表构建

使用主题色构建笔记列表。

        return ListView.builder(
          padding: EdgeInsets.all(12.w),
          itemCount: notes.length,
          itemBuilder: (context, index) {
            final note = notes[index];
            return Card(
              color: theme.cardColor,
              child: ListTile(

ListView.builder构建笔记列表,Card使用主题的卡片颜色。在深色模式下,cardColor会自动变为深色,在浅色模式下则为浅色。ListTile是Material Design的标准列表项组件,提供了标题、副标题和点击交互。这种基于主题的颜色配置让卡片在不同模式下都有合适的对比度。

主题化文字显示

使用主题色显示笔记标题和内容。

                title: Text(
                  note.title.isEmpty ? '无标题' : note.title,
                  style: TextStyle(color: theme.textTheme.titleLarge?.color),
                ),
                subtitle: Text(
                  note.content.isEmpty ? '无内容' : note.content,
                  style: TextStyle(color: theme.textTheme.bodyMedium?.color),
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),

标题使用titleLarge的颜色,副标题使用bodyMedium的颜色。这些颜色由主题系统管理,在深色模式下自动变为浅色文字,在浅色模式下为深色文字。maxLines限制副标题最多显示2行,overflow设为ellipsis在文字过长时显示省略号。这种设计确保了文字在任何主题下都清晰可读。

完成主题适配

完成主题化页面的构建。

                onTap: () => Get.to(() => NoteEditorPage(note: note)),
              ),
            );
          },
        );
      }),
    );
  }
}

ListTile的onTap回调跳转到编辑页面,保持了与其他版本一致的交互。整个页面的所有颜色都从主题中获取,包括背景色、卡片色、文字色和图标色。这种完全主题化的设计让页面能够无缝适配应用的主题切换,用户切换深色/浅色模式时,页面会自动调整所有颜色,提供一致的视觉体验。

总结

文件夹详情页面是笔记应用中重要的功能模块,承载着笔记组织和管理的核心功能。通过本文的详细介绍,我们实现了一个功能完整、交互流畅的文件夹详情系统。

从基础的笔记列表展示,到高级的搜索过滤、批量操作和统计分析,每个功能都经过精心设计。响应式的UI更新、友好的空状态提示、直观的手势交互,这些细节共同构成了优秀的用户体验。主题适配功能确保了页面在不同显示模式下都有良好的可读性。

代码结构清晰,组件化设计提高了复用性和可维护性。GetX状态管理简化了数据流动,ScreenUtil确保了多设备适配。这些技术选型和设计模式为构建专业级的移动应用提供了最佳实践参考。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐