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开发的大部分核心知识。希望这篇文章能帮你掌握这些技能!


相关推荐
youyin5 天前
HarmonyOS ArkUI 组件与自定义组件零基础:从搭页面到组件化开发
华为·harmonyos
HwJack206 天前
【HarmonyOS开发小实践】ArkTS 从 TypeScript 到方舟语言的演进
华为·harmonyos
张星河zen6 天前
4096卡、8EFLOPS、7.2T NPO:华为昇腾960超节点详解
华为·ai训练·ai推理·超节点·npo·昇腾960·hi-one
君赏6 天前
棉宇宙 Flutter 热更新是如何落地的
flutter
云运维笔记6 天前
华为设备IP地址配置全攻略
运维·网络·计算机网络·华为
威哥爱编程6 天前
HarmonyOS 7 星盾机密风控实战:设备风险因子端侧融合计算,可用不可见
harmonyos·arkts
恋猫de小郭6 天前
AndroidX 新增 Security State ,支持可编程的系统安全状态查询
android·前端·flutter
威哥爱编程6 天前
HarmonyOS 7 应用快启实战:关键资源预加载进内存,冷启告别白屏
华为·harmonyos·arkts
zihan5186 天前
自己做软件日记9/21 得到了一个良好的文件结构框架
后端·flutter·前端框架·android studio