在这里插入图片描述

说起备忘录这个功能,我自己是深有体会的。以前总是觉得脑子够用,什么事情都记在心里,结果经常忘东忘西。后来发现,好记性不如烂笔头,一个好用的备忘录功能真的能帮大忙。

为什么需要备忘录功能

你可能会问,手机自带的备忘录不够用吗?说实话,系统自带的备忘录功能确实能用,但是和我们的生活助手App集成在一起,就能实现更多联动。比如:

  • 快速记录:打开App就能记,不用切换应用
  • 分类管理:可以按颜色、标签分类,一目了然
  • 关联提醒:可以和待办事项、日程关联起来
  • 数据同步:所有数据都在一个App里,备份恢复更方便

我自己在开发这个功能的时候,就想着要做得简单好用,不要太复杂。

页面布局设计

备忘录的页面布局我采用了网格布局,这样可以一眼看到多条备忘录,而且每条备忘录都有自己的颜色标识。先来看看基本的页面结构:

class NotesPage extends StatelessWidget {
  const NotesPage({super.key});

  
  Widget build(BuildContext context) {
    final notes = [
      {'title': '会议记录', 'content': '今天的会议讨论了项目进度...', 
       'date': DateTime.now(), 'color': Colors.blue},
      {'title': '学习笔记', 'content': 'Flutter开发要点总结...', 
       'date': DateTime.now().subtract(const Duration(days: 1)), 
       'color': Colors.purple},
    ];

这里我用了一个简单的List来存储备忘录数据。每条备忘录包含标题、内容、日期和颜色四个基本属性。颜色这个设计是我后来加上的,因为发现纯文字的备忘录看起来太单调了,加上颜色之后,不同类型的备忘录一眼就能区分开。

AppBar设计

return Scaffold(
  appBar: AppBar(
    title: const Text('备忘录'),
    actions: [
      IconButton(
        icon: const Icon(Icons.search),
        onPressed: () {},
      ),
    ],
  ),

AppBar很简单,就是一个标题加一个搜索按钮。搜索功能在备忘录多了之后特别有用,别小看这个搜索按钮,它能让你快速找到想要的内容。

网格布局实现

备忘录的展示我用了GridView.builder,这是Flutter中实现网格布局的标准方式:

body: GridView.builder(
  padding: EdgeInsets.all(16.w),
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    crossAxisSpacing: 12.w,
    mainAxisSpacing: 12.h,
    childAspectRatio: 0.8,
  ),
  itemCount: notes.length,
  itemBuilder: (context, index) {
    final note = notes[index];
    return Container(
      // 备忘录卡片内容
    );
  },
),

这里的crossAxisCount: 2表示每行显示2个备忘录卡片。childAspectRatio: 0.8是宽高比,我调了好几次才找到这个合适的比例,太宽了显示不下内容,太窄了又浪费空间

备忘录卡片设计

每个备忘录卡片的设计是这样的:

return Container(
  padding: EdgeInsets.all(16.w),
  decoration: BoxDecoration(
    color: (note['color'] as Color).withOpacity(0.1),
    borderRadius: BorderRadius.circular(12.r),
    border: Border.all(color: note['color'] as Color, width: 2),
  ),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        note['title'] as String,
        style: TextStyle(
          fontSize: 16.sp,
          fontWeight: FontWeight.bold,
        ),
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
      ),

这里有个小技巧:背景色用了半透明的颜色withOpacity(0.1)),边框用了实色。这样既能看出颜色分类,又不会太刺眼。我一开始用的是实色背景,后来发现文字看不清楚,改成半透明之后效果好多了。

内容展示优化

备忘录的内容展示需要考虑文字过长的情况:

SizedBox(height: 8.h),
Expanded(
  child: Text(
    note['content'] as String,
    style: TextStyle(fontSize: 12.sp, color: Colors.grey[700]),
    maxLines: 5,
    overflow: TextOverflow.ellipsis,
  ),
),
SizedBox(height: 8.h),
Text(
  DateFormat('MM-dd HH:mm').format(note['date'] as DateTime),
  style: TextStyle(fontSize: 10.sp, color: Colors.grey),
),

这里用Expanded包裹内容文本,让它占据剩余空间。maxLines: 5限制最多显示5行,超出的部分用省略号表示。日期格式化用的是intl包的DateFormat,显示月-日 时:分的格式,简洁明了。

添加备忘录按钮

页面底部有个浮动按钮,用来添加新的备忘录:

floatingActionButton: FloatingActionButton(
  onPressed: () {},
  child: const Icon(Icons.add),
),

这个按钮的位置是Flutter自动处理的,会显示在右下角。点击后应该弹出一个编辑页面,让用户输入标题、内容,选择颜色等。

数据存储方案

备忘录的数据需要持久化存储,我用的是shared_preferences

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class NotesStorage {
  static const String _key = 'notes_data';
  
  static Future<void> saveNotes(List<Map<String, dynamic>> notes) async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = jsonEncode(notes);
    await prefs.setString(_key, jsonString);
  }
  
  static Future<List<Map<String, dynamic>>> loadNotes() async {
    final prefs = await SharedPreferences.getInstance();
    final jsonString = prefs.getString(_key);
    if (jsonString == null) return [];
    return List<Map<String, dynamic>>.from(jsonDecode(jsonString));
  }
}

这个存储方案很简单,就是把备忘录列表转成JSON字符串存起来。读取的时候再解析回来。虽然简单,但对于备忘录这种数据量不大的场景来说,完全够用了。

颜色选择功能

添加备忘录的时候,需要让用户选择颜色:

final colors = [
  Colors.blue,
  Colors.purple,
  Colors.orange,
  Colors.green,
  Colors.pink,
  Colors.red,
];

Widget buildColorPicker(Color selectedColor, Function(Color) onColorSelected) {
  return Wrap(
    spacing: 12.w,
    children: colors.map((color) {
      final isSelected = color == selectedColor;
      return GestureDetector(
        onTap: () => onColorSelected(color),
        child: Container(
          width: 40.w,
          height: 40.w,
          decoration: BoxDecoration(
            color: color,
            shape: BoxShape.circle,
            border: isSelected ? Border.all(color: Colors.black, width: 3) : null,
          ),
        ),
      );
    }).toList(),
  );
}

这个颜色选择器用Wrap布局,可以自动换行。每个颜色是一个圆形按钮,选中的颜色会有黑色边框,这样用户就知道当前选的是哪个颜色。

搜索功能实现

搜索功能是备忘录的重要功能之一:

List<Map<String, dynamic>> searchNotes(String query, List<Map<String, dynamic>> notes) {
  if (query.isEmpty) return notes;
  
  return notes.where((note) {
    final title = note['title'] as String;
    final content = note['content'] as String;
    return title.toLowerCase().contains(query.toLowerCase()) ||
           content.toLowerCase().contains(query.toLowerCase());
  }).toList();
}

搜索逻辑很简单,就是在标题和内容中查找关键词。不区分大小写,这样搜索起来更方便。我自己用的时候发现,有时候记不清楚是大写还是小写,不区分大小写就方便多了。

编辑和删除功能

每个备忘录卡片应该支持长按编辑或删除:

return GestureDetector(
  onTap: () {
    // 查看详情
  },
  onLongPress: () {
    showModalBottomSheet(
      context: context,
      builder: (context) => Container(
        padding: EdgeInsets.all(16.w),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: const Icon(Icons.edit),
              title: const Text('编辑'),
              onTap: () {
                // 编辑备忘录
              },
            ),
            ListTile(
              leading: const Icon(Icons.delete, color: Colors.red),
              title: const Text('删除', style: TextStyle(color: Colors.red)),
              onTap: () {
                // 删除备忘录
              },
            ),
          ],
        ),
      ),
    );
  },
  child: Container(
    // 备忘录卡片
  ),
);

长按弹出底部菜单,提供编辑和删除选项。这个交互方式比较符合用户习惯,不会误触,又很方便

性能优化建议

备忘录功能虽然简单,但也有一些优化点:

1. 懒加载:如果备忘录数量很多,可以考虑分页加载,不要一次性加载所有数据。

2. 缓存机制:把常用的备忘录缓存在内存中,减少读取次数。

3. 图片支持:可以考虑支持在备忘录中插入图片,这样记录会更丰富。不过要注意图片的压缩和存储。

4. 云同步:如果有多设备使用需求,可以考虑接入云存储服务,实现数据同步。

实际使用体验

我自己用了一段时间这个备忘录功能,感觉还是挺好用的。特别是颜色分类这个功能,让我可以快速区分工作笔记、学习笔记、生活记录等不同类型的内容。

有时候开会的时候,拿出手机快速记几条要点,会后再整理成详细的会议记录。有时候突然想到什么创意,也会马上记下来,免得转头就忘了。

不过也发现了一些可以改进的地方:

  • 富文本编辑:目前只支持纯文本,如果能支持加粗、斜体、列表等格式就更好了
  • 标签系统:除了颜色分类,再加上标签功能会更灵活
  • 语音输入:有时候打字不方便,语音输入会更快
  • 提醒功能:某些备忘录可以设置提醒时间,到时候自动通知

添加备忘录页面

点击浮动按钮后,需要跳转到添加页面。这个页面的设计也很重要:

class AddNotePage extends StatefulWidget {
  const AddNotePage({super.key});

  
  State<AddNotePage> createState() => _AddNotePageState();
}

class _AddNotePageState extends State<AddNotePage> {
  final _titleController = TextEditingController();
  final _contentController = TextEditingController();
  Color _selectedColor = Colors.blue;
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('新建备忘录'),
        actions: [
          TextButton(
            onPressed: _saveNote,
            child: Text('保存', style: TextStyle(color: Colors.white, fontSize: 16.sp)),
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildTitleInput(),
            SizedBox(height: 16.h),
            _buildContentInput(),
            SizedBox(height: 24.h),
            _buildColorSelector(),
          ],
        ),
      ),
    );
  }
}

这个页面用了StatefulWidget,因为需要管理输入框的内容和选中的颜色。右上角的保存按钮用TextButton实现,点击后保存备忘录。

标题输入框

Widget _buildTitleInput() {
  return TextField(
    controller: _titleController,
    decoration: InputDecoration(
      hintText: '标题',
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12.r),
      ),
      filled: true,
      fillColor: Colors.grey[100],
    ),
    style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
    maxLength: 50,
  );
}

标题输入框限制了最多50个字符,这样可以避免标题太长。输入框的背景色用了浅灰色,和白色背景形成对比,让用户知道这是可以输入的区域。

内容输入框

Widget _buildContentInput() {
  return TextField(
    controller: _contentController,
    decoration: InputDecoration(
      hintText: '内容',
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12.r),
      ),
      filled: true,
      fillColor: Colors.grey[100],
    ),
    style: TextStyle(fontSize: 14.sp),
    maxLines: 10,
    minLines: 5,
  );
}

内容输入框设置了maxLines: 10minLines: 5,这样输入框会有一个合适的初始高度,输入内容多了会自动扩展。我试过很多组合,5到10行是最舒服的

颜色选择器

Widget _buildColorSelector() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        '选择颜色',
        style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
      ),
      SizedBox(height: 12.h),
      Wrap(
        spacing: 12.w,
        children: [
          Colors.blue,
          Colors.purple,
          Colors.orange,
          Colors.green,
          Colors.pink,
          Colors.red,
        ].map((color) {
          final isSelected = color == _selectedColor;
          return GestureDetector(
            onTap: () => setState(() => _selectedColor = color),
            child: Container(
              width: 50.w,
              height: 50.w,
              decoration: BoxDecoration(
                color: color,
                shape: BoxShape.circle,
                border: isSelected 
                  ? Border.all(color: Colors.black, width: 3) 
                  : null,
              ),
              child: isSelected 
                ? Icon(Icons.check, color: Colors.white, size: 24.sp)
                : null,
            ),
          );
        }).toList(),
      ),
    ],
  );
}

颜色选择器在选中的颜色上加了一个白色的对勾图标,这样视觉反馈更明确。用户点击颜色圆圈时,会调用setState更新选中状态。

保存备忘录逻辑

保存按钮的逻辑需要验证输入并保存数据:

Future<void> _saveNote() async {
  final title = _titleController.text.trim();
  final content = _contentController.text.trim();
  
  if (title.isEmpty) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请输入标题')),
    );
    return;
  }
  
  if (content.isEmpty) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请输入内容')),
    );
    return;
  }
  
  final note = {
    'id': DateTime.now().millisecondsSinceEpoch.toString(),
    'title': title,
    'content': content,
    'color': _selectedColor.value,
    'date': DateTime.now().toIso8601String(),
  };
  
  // 保存到本地存储
  final notes = await NotesStorage.loadNotes();
  notes.insert(0, note);
  await NotesStorage.saveNotes(notes);
  
  if (mounted) {
    Navigator.pop(context, true);
  }
}

这里做了几个验证:标题和内容都不能为空。保存时给每个备忘录生成一个唯一的ID,用时间戳来实现。新的备忘录插入到列表开头,这样最新的备忘录会显示在最前面。

编辑备忘录功能

编辑功能和添加类似,只是需要预填充数据:

class EditNotePage extends StatefulWidget {
  final Map<String, dynamic> note;
  
  const EditNotePage({super.key, required this.note});

  
  State<EditNotePage> createState() => _EditNotePageState();
}

class _EditNotePageState extends State<EditNotePage> {
  late TextEditingController _titleController;
  late TextEditingController _contentController;
  late Color _selectedColor;
  
  
  void initState() {
    super.initState();
    _titleController = TextEditingController(text: widget.note['title']);
    _contentController = TextEditingController(text: widget.note['content']);
    _selectedColor = Color(widget.note['color'] as int);
  }
  
  
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

initState中初始化输入框的内容和颜色。别忘了在dispose中释放控制器,避免内存泄漏。

删除确认对话框

删除备忘录时应该弹出确认对话框:

Future<void> _showDeleteDialog(BuildContext context, String noteId) async {
  final result = await showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('删除备忘录'),
      content: const Text('确定要删除这条备忘录吗?删除后无法恢复。'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context, true),
          style: TextButton.styleFrom(foregroundColor: Colors.red),
          child: const Text('删除'),
        ),
      ],
    ),
  );
  
  if (result == true) {
    await _deleteNote(noteId);
  }
}

Future<void> _deleteNote(String noteId) async {
  final notes = await NotesStorage.loadNotes();
  notes.removeWhere((note) => note['id'] == noteId);
  await NotesStorage.saveNotes(notes);
  
  if (mounted) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('备忘录已删除')),
    );
  }
}

删除按钮用红色,警示用户这是危险操作。对话框返回布尔值,只有用户点击"删除"按钮时才真正执行删除。

搜索页面实现

搜索功能需要一个专门的页面:

class SearchNotesPage extends StatefulWidget {
  const SearchNotesPage({super.key});

  
  State<SearchNotesPage> createState() => _SearchNotesPageState();
}

class _SearchNotesPageState extends State<SearchNotesPage> {
  final _searchController = TextEditingController();
  List<Map<String, dynamic>> _searchResults = [];
  List<Map<String, dynamic>> _allNotes = [];
  
  
  void initState() {
    super.initState();
    _loadNotes();
  }
  
  Future<void> _loadNotes() async {
    final notes = await NotesStorage.loadNotes();
    setState(() {
      _allNotes = notes;
    });
  }
  
  void _performSearch(String query) {
    if (query.isEmpty) {
      setState(() {
        _searchResults = [];
      });
      return;
    }
    
    final results = _allNotes.where((note) {
      final title = (note['title'] as String).toLowerCase();
      final content = (note['content'] as String).toLowerCase();
      final searchQuery = query.toLowerCase();
      return title.contains(searchQuery) || content.contains(searchQuery);
    }).toList();
    
    setState(() {
      _searchResults = results;
    });
  }

搜索页面用TextFieldonChanged回调实时搜索,用户每输入一个字符就更新搜索结果。这种即时搜索的体验比点击搜索按钮要好。

备忘录详情页

点击备忘录卡片应该进入详情页,可以查看完整内容:

class NoteDetailPage extends StatelessWidget {
  final Map<String, dynamic> note;
  
  const NoteDetailPage({super.key, required this.note});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('备忘录详情'),
        actions: [
          IconButton(
            icon: const Icon(Icons.edit),
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => EditNotePage(note: note),
                ),
              );
            },
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              note['title'] as String,
              style: TextStyle(
                fontSize: 24.sp,
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(height: 8.h),
            Text(
              DateFormat('yyyy-MM-dd HH:mm').format(
                DateTime.parse(note['date'] as String),
              ),
              style: TextStyle(fontSize: 12.sp, color: Colors.grey),
            ),
            SizedBox(height: 24.h),
            Text(
              note['content'] as String,
              style: TextStyle(fontSize: 16.sp, height: 1.5),
            ),
          ],
        ),
      ),
    );
  }
}

详情页很简单,就是显示标题、日期和完整内容。右上角有个编辑按钮,方便用户快速编辑。

数据模型优化

为了代码更规范,可以定义一个Note类:

class Note {
  final String id;
  final String title;
  final String content;
  final Color color;
  final DateTime date;
  
  Note({
    required this.id,
    required this.title,
    required this.content,
    required this.color,
    required this.date,
  });
  
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'content': content,
      'color': color.value,
      'date': date.toIso8601String(),
    };
  }
  
  factory Note.fromJson(Map<String, dynamic> json) {
    return Note(
      id: json['id'] as String,
      title: json['title'] as String,
      content: json['content'] as String,
      color: Color(json['color'] as int),
      date: DateTime.parse(json['date'] as String),
    );
  }
}

有了这个类,代码会更清晰,类型安全也更好。不用到处写as Stringas int这样的类型转换。

排序功能

备忘录可以按不同方式排序:

enum SortType {
  dateDesc,  // 按日期降序
  dateAsc,   // 按日期升序
  titleAsc,  // 按标题升序
}

List<Note> sortNotes(List<Note> notes, SortType sortType) {
  final sortedNotes = List<Note>.from(notes);
  
  switch (sortType) {
    case SortType.dateDesc:
      sortedNotes.sort((a, b) => b.date.compareTo(a.date));
      break;
    case SortType.dateAsc:
      sortedNotes.sort((a, b) => a.date.compareTo(b.date));
      break;
    case SortType.titleAsc:
      sortedNotes.sort((a, b) => a.title.compareTo(b.title));
      break;
  }
  
  return sortedNotes;
}

可以在AppBar加个排序按钮,让用户选择排序方式。默认按日期降序,最新的备忘录显示在前面。

总结

备忘录功能看起来简单,但要做好也不容易。关键是要简单易用,让用户能够快速记录,快速查找。我在实现这个功能的时候,一直在思考怎么让它更好用,怎么和其他功能更好地集成。

后来发现,好的工具应该是无感的,用户不需要思考怎么用,拿起来就能用。这也是我在设计所有功能时的一个原则。

从我自己的使用体验来看,颜色分类、快速搜索、简洁的编辑界面,这些都是让备忘录好用的关键。特别是颜色分类,让我可以一眼区分不同类型的笔记,效率提高了不少。

如果你也在开发类似的功能,建议多从用户角度思考,多试用,多改进。一个好用的备忘录功能,真的能让生活变得更有条理。

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

Logo

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

更多推荐