Flutter for OpenHarmony生活助手App实战:备忘录功能实现

说起备忘录这个功能,我自己是深有体会的。以前总是觉得脑子够用,什么事情都记在心里,结果经常忘东忘西。后来发现,好记性不如烂笔头,一个好用的备忘录功能真的能帮大忙。
为什么需要备忘录功能
你可能会问,手机自带的备忘录不够用吗?说实话,系统自带的备忘录功能确实能用,但是和我们的生活助手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: 10和minLines: 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;
});
}
搜索页面用TextField的onChanged回调实时搜索,用户每输入一个字符就更新搜索结果。这种即时搜索的体验比点击搜索按钮要好。
备忘录详情页
点击备忘录卡片应该进入详情页,可以查看完整内容:
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 String、as 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
更多推荐
所有评论(0)