Flutter 框架跨平台鸿蒙开发 - 打造精美记事本备忘录应用

📝 Flutter + HarmonyOS 实战:打造精美记事本备忘录应用


效果预览图

📋 文章导读

章节 内容概要 预计阅读
记事本应用需求分析 3分钟
数据模型与本地存储 8分钟
笔记列表页面实现 10分钟
笔记编辑页面实现 8分钟
搜索与筛选功能 5分钟
完整源码与运行 3分钟

💡 写在前面:记事本是每个人手机里的必备应用。一个好的记事本不仅要能记录文字,还要支持分类、搜索、个性化等功能。本文将带你用Flutter从零打造一款功能完善、界面精美的记事本应用,涵盖本地存储、状态管理、页面导航等核心知识点。


一、需求分析与设计

1.1 功能需求

记事本应用
笔记管理
新建笔记
编辑笔记
删除笔记
置顶笔记
展示方式
网格视图
列表视图
搜索过滤
个性化
多种背景色
深色模式
数据存储
本地持久化
自动保存

1.2 界面设计

页面 功能 核心组件
主页 笔记列表展示 GridView / ListView
编辑页 创建/编辑笔记 TextField
搜索 关键词过滤 SearchBar
选项 置顶/删除操作 BottomSheet

1.3 配色方案

笔记支持8种背景色,让用户可以分类标记:

颜色 色值 适用场景
⬜ 白色 #FFFFFF 默认
🟨 浅黄 #FFF9C4 重要事项
🟧 浅橙 #FFCCBC 工作相关
🩷 浅粉 #F8BBD9 生活记录
🟪 浅紫 #E1BEE7 灵感创意
🟦 浅蓝 #BBDEFB 学习笔记
🩵 浅青 #B2DFDB 健康运动
🟩 浅绿 #C8E6C9 财务相关

二、数据模型与本地存储

2.1 笔记数据模型

dart 复制代码
class Note {
  String id;           // 唯一标识
  String title;        // 标题
  String content;      // 内容
  DateTime createdAt;  // 创建时间
  DateTime updatedAt;  // 更新时间
  int colorIndex;      // 颜色索引
  bool isPinned;       // 是否置顶

  Note({
    required this.id,
    required this.title,
    required this.content,
    required this.createdAt,
    required this.updatedAt,
    this.colorIndex = 0,
    this.isPinned = false,
  });
}

2.2 JSON序列化

为了实现本地存储,需要将对象转换为JSON:

dart 复制代码
// 对象 → JSON
Map<String, dynamic> toJson() => {
  'id': id,
  'title': title,
  'content': content,
  'createdAt': createdAt.toIso8601String(),
  'updatedAt': updatedAt.toIso8601String(),
  'colorIndex': colorIndex,
  'isPinned': isPinned,
};

// JSON → 对象
factory Note.fromJson(Map<String, dynamic> json) => Note(
  id: json['id'],
  title: json['title'],
  content: json['content'],
  createdAt: DateTime.parse(json['createdAt']),
  updatedAt: DateTime.parse(json['updatedAt']),
  colorIndex: json['colorIndex'] ?? 0,
  isPinned: json['isPinned'] ?? false,
);

2.3 SharedPreferences 存储

dart 复制代码
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

/// 加载笔记
Future<void> _loadNotes() async {
  final prefs = await SharedPreferences.getInstance();
  final notesJson = prefs.getString('notes');
  
  if (notesJson != null) {
    final List<dynamic> decoded = jsonDecode(notesJson);
    notes = decoded.map((e) => Note.fromJson(e)).toList();
  }
}

/// 保存笔记
Future<void> _saveNotes() async {
  final prefs = await SharedPreferences.getInstance();
  final notesJson = jsonEncode(notes.map((e) => e.toJson()).toList());
  await prefs.setString('notes', notesJson);
}

2.4 数据流程图

toJson
jsonEncode
SharedPreferences
getString
jsonDecode
fromJson
Note对象
Map
JSON字符串
本地存储


三、笔记列表页面

3.1 页面结构

Body
Scaffold
AppBar - 标题 + 视图切换
Body
FAB - 新建笔记
搜索栏
笔记列表/网格

3.2 视图切换

支持网格视图和列表视图两种展示方式:

dart 复制代码
bool isGridView = true;

// 切换按钮
IconButton(
  icon: Icon(isGridView ? Icons.view_list : Icons.grid_view),
  onPressed: () => setState(() => isGridView = !isGridView),
)

// 条件渲染
if (isGridView) {
  return GridView.builder(...);
} else {
  return ListView.builder(...);
}

3.3 网格视图实现

dart 复制代码
GridView.builder(
  padding: const EdgeInsets.all(16),
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,        // 每行2个
    crossAxisSpacing: 12,     // 横向间距
    mainAxisSpacing: 12,      // 纵向间距
    childAspectRatio: 0.85,   // 宽高比
  ),
  itemCount: filteredNotes.length,
  itemBuilder: (context, index) => _buildNoteCard(filteredNotes[index]),
)

3.4 笔记卡片设计

dart 复制代码
Widget _buildNoteCard(Note note) {
  return GestureDetector(
    onTap: () => _editNote(note),
    onLongPress: () => _showNoteOptions(note),
    child: Container(
      decoration: BoxDecoration(
        color: NoteColors.colors[note.colorIndex],
        borderRadius: BorderRadius.circular(16),
        boxShadow: [...],
      ),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 置顶图标 + 时间
            Row(children: [
              if (note.isPinned) Icon(Icons.push_pin, size: 16),
              Spacer(),
              Text(_formatDate(note.updatedAt)),
            ]),
            // 标题
            Text(note.title, style: TextStyle(fontWeight: FontWeight.bold)),
            // 内容预览
            Expanded(child: Text(note.content, maxLines: 5)),
          ],
        ),
      ),
    ),
  );
}

3.5 排序逻辑

笔记按照"置顶优先 + 更新时间倒序"排列:

dart 复制代码
void _filterNotes() {
  // 先过滤
  if (searchQuery.isEmpty) {
    filteredNotes = List.from(notes);
  } else {
    filteredNotes = notes.where((note) =>
      note.title.toLowerCase().contains(searchQuery.toLowerCase()) ||
      note.content.toLowerCase().contains(searchQuery.toLowerCase())
    ).toList();
  }
  
  // 再排序
  filteredNotes.sort((a, b) {
    // 置顶的排前面
    if (a.isPinned && !b.isPinned) return -1;
    if (!a.isPinned && b.isPinned) return 1;
    // 同级别按更新时间倒序
    return b.updatedAt.compareTo(a.updatedAt);
  });
}

四、笔记编辑页面

4.1 页面导航

使用 Navigator.push 进行页面跳转,并通过返回值传递数据:

dart 复制代码
// 跳转到编辑页
Future<void> _addNote() async {
  final result = await Navigator.push<Note>(
    context,
    MaterialPageRoute(builder: (_) => const NoteEditPage()),
  );
  
  // 处理返回结果
  if (result != null) {
    setState(() {
      notes.add(result);
      _filterNotes();
    });
    _saveNotes();
  }
}

4.2 编辑页面布局

dart 复制代码
Scaffold(
  backgroundColor: NoteColors.colors[_colorIndex],  // 动态背景色
  appBar: AppBar(
    backgroundColor: NoteColors.colors[_colorIndex],
    title: Text(widget.note == null ? '新建笔记' : '编辑笔记'),
    actions: [
      IconButton(icon: Icon(Icons.palette), onPressed: _showColorPicker),
      IconButton(icon: Icon(Icons.check), onPressed: _saveNote),
    ],
  ),
  body: Column(
    children: [
      // 标题输入框
      TextField(
        controller: _titleController,
        style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
        decoration: InputDecoration(hintText: '标题', border: InputBorder.none),
      ),
      Divider(),
      // 内容输入框
      TextField(
        controller: _contentController,
        maxLines: null,
        minLines: 20,
        decoration: InputDecoration(hintText: '开始输入...', border: InputBorder.none),
      ),
    ],
  ),
  bottomNavigationBar: _buildBottomBar(),  // 字数统计
)

4.3 颜色选择器

dart 复制代码
void _showColorPicker() {
  showModalBottomSheet(
    context: context,
    builder: (context) => Padding(
      padding: const EdgeInsets.all(20),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text('选择背景颜色', style: TextStyle(fontSize: 18)),
          SizedBox(height: 16),
          Wrap(
            spacing: 12,
            runSpacing: 12,
            children: List.generate(NoteColors.colors.length, (index) {
              return GestureDetector(
                onTap: () {
                  setState(() => _colorIndex = index);
                  Navigator.pop(context);
                },
                child: Container(
                  width: 48,
                  height: 48,
                  decoration: BoxDecoration(
                    color: NoteColors.colors[index],
                    shape: BoxShape.circle,
                    border: Border.all(
                      color: _colorIndex == index ? Colors.blue : Colors.grey,
                      width: _colorIndex == index ? 3 : 1,
                    ),
                  ),
                  child: _colorIndex == index ? Icon(Icons.check) : null,
                ),
              );
            }),
          ),
        ],
      ),
    ),
  );
}

4.4 未保存提醒

使用 PopScope 拦截返回操作:

dart 复制代码
PopScope(
  canPop: !_hasChanges,  // 有更改时不允许直接返回
  onPopInvokedWithResult: (didPop, result) async {
    if (didPop) return;
    
    // 显示确认对话框
    final shouldPop = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('放弃更改?'),
        content: Text('你有未保存的更改,确定要放弃吗?'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context, false), child: Text('继续编辑')),
          TextButton(onPressed: () => Navigator.pop(context, true), child: Text('放弃')),
        ],
      ),
    );
    
    if (shouldPop == true && context.mounted) {
      Navigator.pop(context);
    }
  },
  child: Scaffold(...),
)

五、搜索与筛选

5.1 搜索栏实现

dart 复制代码
TextField(
  controller: _searchController,
  decoration: InputDecoration(
    hintText: '搜索笔记...',
    prefixIcon: Icon(Icons.search),
    suffixIcon: searchQuery.isNotEmpty
        ? IconButton(
            icon: Icon(Icons.clear),
            onPressed: () {
              _searchController.clear();
              setState(() {
                searchQuery = '';
                _filterNotes();
              });
            },
          )
        : null,
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
      borderSide: BorderSide.none,
    ),
    filled: true,
  ),
  onChanged: (value) {
    setState(() {
      searchQuery = value;
      _filterNotes();
    });
  },
)

5.2 搜索逻辑

同时搜索标题和内容,不区分大小写:

dart 复制代码
filteredNotes = notes.where((note) =>
  note.title.toLowerCase().contains(searchQuery.toLowerCase()) ||
  note.content.toLowerCase().contains(searchQuery.toLowerCase())
).toList();

5.3 空状态处理

dart 复制代码
Widget _buildEmptyState() {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(
          searchQuery.isEmpty ? Icons.note_add : Icons.search_off,
          size: 80,
          color: Colors.grey,
        ),
        SizedBox(height: 16),
        Text(
          searchQuery.isEmpty ? '还没有笔记' : '没有找到相关笔记',
          style: TextStyle(fontSize: 18, color: Colors.grey),
        ),
        if (searchQuery.isEmpty)
          Text('点击下方按钮创建第一条笔记'),
      ],
    ),
  );
}

六、完整源码与运行

6.1 项目结构

复制代码
flutter_notes/
├── lib/
│   └── main.dart       # 记事本应用代码(约450行)
├── ohos/               # 鸿蒙平台配置
├── pubspec.yaml        # 依赖配置
└── README.md           # 项目说明

6.2 依赖配置

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.2.2  # 本地存储

6.3 运行命令

bash 复制代码
# 获取依赖
flutter pub get

# 运行应用
flutter run

# 运行到鸿蒙设备
flutter run -d ohos

6.4 功能清单

功能 状态 说明
新建笔记 标题+内容
编辑笔记 点击进入编辑
删除笔记 长按或菜单
置顶笔记 重要笔记置顶
搜索笔记 标题+内容搜索
网格/列表视图 一键切换
8种背景色 个性化分类
本地存储 SharedPreferences
未保存提醒 防止误操作
字数统计 底部显示
时间显示 相对时间
深色模式 跟随系统

七、扩展方向

7.1 功能扩展

记事本
分类标签
图片附件
云端同步
Markdown支持
提醒功能
自定义标签
相册选择
账号登录
富文本编辑
定时提醒

7.2 分类标签实现思路

dart 复制代码
class Note {
  // ... 原有字段
  List<String> tags;  // 新增标签字段
}

// 标签筛选
filteredNotes = notes.where((note) =>
  selectedTag == null || note.tags.contains(selectedTag)
).toList();

7.3 图片附件思路

dart 复制代码
// 使用 image_picker 包
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);

// 存储图片路径
class Note {
  List<String> imagePaths;
}

八、常见问题

Q1: 为什么用 SharedPreferences 而不是 SQLite?

对于简单的笔记应用,SharedPreferences 足够使用:

  • 优点:API简单,无需建表
  • 缺点:不支持复杂查询,数据量大时性能下降

如果笔记数量超过100条或需要复杂查询,建议使用 SQLite(sqflite包)或 Hive。
Q2: 如何实现笔记的云端同步?

可以使用以下方案:

  1. Firebase Firestore - Google官方,集成简单
  2. Supabase - 开源替代方案
  3. 自建后端 - 完全控制数据

基本流程:登录 → 上传本地数据 → 下载云端数据 → 合并冲突
Q3: 如何支持 Markdown 格式?

可以使用 flutter_markdown 包:

dart 复制代码
import 'package:flutter_markdown/flutter_markdown.dart';

// 预览模式
Markdown(data: note.content)

// 编辑模式
TextField(controller: _contentController)

九、总结

本文从零实现了一款功能完善的记事本应用,核心技术点包括:

  1. 数据模型:Note类设计与JSON序列化
  2. 本地存储:SharedPreferences持久化
  3. 列表展示:GridView/ListView双视图
  4. 页面导航:Navigator传值与返回
  5. 搜索过滤:实时搜索与排序
  6. 交互细节:颜色选择、未保存提醒、置顶功能

记事本虽然是一个常见的应用类型,但涵盖了Flutter开发的大部分核心知识。希望这篇文章能帮你掌握这些技能!


相关推荐
盐焗西兰花6 小时前
鸿蒙学习实战之路-Reader Kit修改翻页方式字体大小及行间距最佳实践
学习·华为·harmonyos
不爱吃糖的程序媛7 小时前
Flutter 与 OpenHarmony 通信:Flutter Channel 使用指南
前端·javascript·flutter
用户66116655296529 小时前
Futter3 仿抖音我的页面or用户详情页
flutter
lbb 小魔仙10 小时前
【HarmonyOS实战】React Native 表单实战:在 OpenHarmony 上构建高性能表单
react native·华为·harmonyos
Haha_bj10 小时前
Flutter ——device_info_plus详解
android·flutter·ios
前端小伙计10 小时前
Android/Flutter 项目统一构建配置最佳实践
android·flutter
微祎_11 小时前
Flutter for OpenHarmony:形状拼图游戏开发全指南 - 基于Flutter CustomPaint的可拖拽矢量拼图实现与设计理念
flutter
不爱吃糖的程序媛11 小时前
解锁Flutter鸿蒙开发新姿势——flutter_ohfeatures插件集实战指南
flutter
一只大侠的侠12 小时前
React Native开源鸿蒙跨平台训练营 Day16自定义 useForm 高性能验证
flutter·开源·harmonyos
子春一12 小时前
Flutter for OpenHarmony:绿氧 - 基于Flutter的呼吸训练应用开发实践与身心交互设计
flutter·交互