《零依赖!用 Flutter + OpenHarmony 构建鸿蒙风格临时记事本(一):内存 CRUD》

个人主页:ujainu

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

文章目录

    • [一、鸿蒙设计语言在 Flutter 中的落地实践](#一、鸿蒙设计语言在 Flutter 中的落地实践)
    • [二、数据模型定义:Note 类](#二、数据模型定义:Note 类)
    • [三、主界面:HomePage(StatefulWidget + ListView)](#三、主界面:HomePage(StatefulWidget + ListView))
      • [1. 状态管理:使用 StatefulWidget](#1. 状态管理:使用 StatefulWidget)
    • [四、列表项:Dismissible 实现左滑删除](#四、列表项:Dismissible 实现左滑删除)
    • 五、新建与编辑:NoteEditorPage
      • [1. 页面跳转与参数传递](#1. 页面跳转与参数传递)
      • [2. NoteEditorPage 实现](#2. NoteEditorPage 实现)
    • [六、完整可运行代码(Flutter + OpenHarmony)](#六、完整可运行代码(Flutter + OpenHarmony))
    • 结语

在移动应用开发的学习路径中,记事本(Notes App) 常被视为"Hello World"级别的实战项目。它虽简单,却完整覆盖了 数据建模、状态管理、页面跳转、用户交互、UI 规范 等核心技能。而当我们将其限定在 Flutter + OpenHarmony 平台,并遵循 鸿蒙设计语言(HarmonyOS Design) 时,更需关注 简洁性、留白感与圆角美学

本文将带你从零构建一个 完全基于内存的临时记事本 ,实现完整的 增(Create)、查(Read)、改(Update)、删(Delete) 四大操作。所有数据仅在 App 运行期间存在于 List<Note> 中,关闭即清空------这正是"零依赖"、"无数据库"的轻量级开发范式。

为什么先做内存版?

  • 快速验证 UI 与交互逻辑
  • 避免早期陷入数据库配置细节
  • 为后续集成持久化(如 Drift / SQLite)打下坚实基础
  • 特别适合 OpenHarmony 轻量设备快速原型开发

一、鸿蒙设计语言在 Flutter 中的落地实践

OpenHarmony 强调 "自然、高效、一致" 的用户体验,其视觉规范可提炼为三大关键词:

  1. 简洁(Simplicity):去除冗余装饰,聚焦内容本身
  2. 留白(Breathing Space):合理间距提升可读性与呼吸感
  3. 圆角(Rounded Corners):柔和边界增强亲和力

在 Flutter 中,我们通过以下方式精准还原:

  • 启用 Material 3useMaterial3: true),获得现代圆角组件
  • 使用 Card 包裹列表项,默认圆角 12px
  • 设置统一内边距(padding: 16)与项间距(margin: bottom 12
  • 文字颜色采用 Colors.grey[800](标题)与 Colors.grey(预览/时间)
  • 图标使用标准 Material Icons(OpenHarmony 兼容良好)

📌 重要提示 :虽然 OpenHarmony 有自研 UI 框架 ArkUI,但 Flutter 应用运行于其上时,遵循 Material Design 即可获得流畅、合规的体验,无需强行模仿原生组件。


二、数据模型定义:Note 类

首先定义笔记的核心结构。由于是内存操作,我们使用普通 Dart 类即可:

dart 复制代码
class Note {
  final String id;
  String title;
  String content;
  final DateTime createdAt;

  Note({
    required this.title,
    this.content = '',
  })  : id = DateTime.now().microsecondsSinceEpoch.toString(),
        createdAt = DateTime.now();

  // 用于编辑时保留原始 ID
  Note.withId({
    required this.id,
    required this.title,
    required this.content,
    required this.createdAt,
  });
}

🔍 关键设计说明

  • id 使用微秒时间戳生成,确保唯一性(内存场景足够)
  • 提供两个构造函数:新建用默认构造,编辑用 withId 保留原 ID
  • createdAt 仅在创建时赋值,不可修改

三、主界面:HomePage(StatefulWidget + ListView)

主界面负责展示笔记列表,并提供 新建入口左滑删除 功能。

1. 状态管理:使用 StatefulWidget

由于数据在内存中动态变化,我们选择最直接的 StatefulWidget

dart 复制代码
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<Note> _notes = []; // 内存数据源

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('我的笔记', style: TextStyle(fontWeight: FontWeight.w500)),
        backgroundColor: Colors.white,
        foregroundColor: Colors.black,
        elevation: 0,
      ),
      body: _buildNoteList(),
      floatingActionButton: FloatingActionButton(
        onPressed: _onAddNote,
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _buildNoteList() {
    if (_notes.isEmpty) {
      return const Center(
        child: Text('暂无笔记', style: TextStyle(color: Colors.grey)),
      );
    }
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: _notes.length,
      itemBuilder: (context, index) {
        final note = _notes[index];
        return _buildNoteItem(note, index);
      },
    );
  }
}

优化点

  • 空状态友好提示
  • 统一 padding: 16 保证留白
  • 使用 ListView.builder 按需渲染,性能更优

四、列表项:Dismissible 实现左滑删除

Flutter 提供 Dismissible 组件,轻松实现 iOS 风格的滑动删除:

dart 复制代码
Widget _buildNoteItem(Note note, int index) {
  return Dismissible(
    key: Key(note.id), // 必须唯一
    direction: DismissDirection.endToStart, // 仅允许左滑
    background: Container(
      color: Colors.red,
      alignment: Alignment.centerRight,
      padding: const EdgeInsets.only(right: 20),
      child: const Icon(Icons.delete, color: Colors.white),
    ),
    onDismissed: (direction) {
      setState(() {
        _notes.removeAt(index);
      });
      // 可选:显示 SnackBar 提示
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('已删除 "${note.title}"')),
      );
    },
    child: _buildNoteCard(note),
  );
}

Widget _buildNoteCard(Note note) {
  return Card(
    margin: const EdgeInsets.only(bottom: 12),
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            note.title,
            style: const TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
              color: Colors.grey,
            ),
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
          const SizedBox(height: 8),
          if (note.content.isNotEmpty)
            Text(
              note.content,
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
              style: const TextStyle(color: Colors.grey),
            ),
          const SizedBox(height: 12),
          Text(
            _formatTime(note.createdAt),
            style: const TextStyle(color: Colors.grey, fontSize: 12),
          ),
        ],
      ),
    ),
  );
}

String _formatTime(DateTime time) {
  return '${time.year}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} '
      '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
}

⚠️ 关键细节

  • key: Key(note.id) 确保 Flutter 能正确识别被删除项
  • background 自定义删除背景(红色 + 删除图标)
  • onDismissed 中调用 setState 触发 UI 更新
  • 使用 ScaffoldMessenger 显示操作反馈

五、新建与编辑:NoteEditorPage

通过 Navigator.push 打开编辑页,并支持 双向数据传递

1. 页面跳转与参数传递

dart 复制代码
// 在 HomePage 中
void _onAddNote() {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (_) => const NoteEditorPage()),
  ).then((result) {
    if (result != null && result is Note) {
      setState(() {
        _notes.insert(0, result); // 插入到顶部
      });
    }
  });
}

// 编辑已有笔记
void _onEditNote(Note note) {
  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (_) => NoteEditorPage(
        existingNote: note,
      ),
    ),
  ).then((result) {
    if (result != null && result is Note) {
      setState(() {
        final index = _notes.indexWhere((n) => n.id == result.id);
        if (index != -1) {
          _notes[index] = result; // 替换原笔记
        }
      });
    }
  });
}

2. NoteEditorPage 实现

dart 复制代码
class NoteEditorPage extends StatefulWidget {
  final Note? existingNote; // null 表示新建

  const NoteEditorPage({this.existingNote, super.key});

  @override
  State<NoteEditorPage> createState() => _NoteEditorPageState();
}

class _NoteEditorPageState extends State<NoteEditorPage> {
  late final TextEditingController _titleController;
  late final TextEditingController _contentController;

  @override
  void initState() {
    super.initState();
    if (widget.existingNote != null) {
      _titleController = TextEditingController(text: widget.existingNote!.title);
      _contentController = TextEditingController(text: widget.existingNote!.content);
    } else {
      _titleController = TextEditingController();
      _contentController = TextEditingController();
    }
  }

  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  void _saveNote() {
    final title = _titleController.text.trim();
    if (title.isEmpty) return;

    final note = widget.existingNote != null
        ? Note.withId(
            id: widget.existingNote!.id,
            title: title,
            content: _contentController.text.trim(),
            createdAt: widget.existingNote!.createdAt,
          )
        : Note(title: title, content: _contentController.text.trim());

    Navigator.pop(context, note);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.existingNote == null ? '新建笔记' : '编辑笔记'),
        backgroundColor: Colors.white,
        foregroundColor: Colors.black,
        elevation: 0,
        actions: [
          IconButton(
            icon: const Icon(Icons.save),
            onPressed: _saveNote,
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: _titleController,
              decoration: const InputDecoration(
                hintText: '标题',
                border: InputBorder.none,
                focusedBorder: InputBorder.none,
              ),
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
              maxLines: 1,
            ),
            const Divider(height: 24),
            Expanded(
              child: TextField(
                controller: _contentController,
                decoration: const InputDecoration(
                  hintText: '写下你的想法...',
                  border: InputBorder.none,
                  focusedBorder: InputBorder.none,
                ),
                maxLines: null,
                keyboardType: TextInputType.multiline,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

关键逻辑

  • 通过 existingNote 区分新建/编辑
  • 编辑时保留原 idcreatedAt
  • 使用 Navigator.pop(context, note) 返回数据
  • 标题为空时不保存(防误操作)

六、完整可运行代码(Flutter + OpenHarmony)

以下代码整合了所有模块,可直接复制到 main.dart 中运行:

dart 复制代码
// main.dart - 内存版鸿蒙记事本(CRUD)
import 'package:flutter/material.dart';

// ==================== 数据模型 ====================
class Note {
  final String id;
  String title;
  String content;
  final DateTime createdAt;

  Note({required this.title, this.content = ''})
      : id = DateTime.now().microsecondsSinceEpoch.toString(),
        createdAt = DateTime.now();

  Note.withId({
    required this.id,
    required this.title,
    required this.content,
    required this.createdAt,
  });
}

// ==================== 编辑页面 ====================
class NoteEditorPage extends StatefulWidget {
  final Note? existingNote;

  const NoteEditorPage({this.existingNote, super.key});

  @override
  State<NoteEditorPage> createState() => _NoteEditorPageState();
}

class _NoteEditorPageState extends State<NoteEditorPage> {
  late final TextEditingController _titleController;
  late final TextEditingController _contentController;

  @override
  void initState() {
    super.initState();
    if (widget.existingNote != null) {
      _titleController = TextEditingController(text: widget.existingNote!.title);
      _contentController = TextEditingController(text: widget.existingNote!.content);
    } else {
      _titleController = TextEditingController();
      _contentController = TextEditingController();
    }
  }

  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  void _saveNote() {
    final title = _titleController.text.trim();
    if (title.isEmpty) return;
    final note = widget.existingNote != null
        ? Note.withId(
            id: widget.existingNote!.id,
            title: title,
            content: _contentController.text.trim(),
            createdAt: widget.existingNote!.createdAt,
          )
        : Note(title: title, content: _contentController.text.trim());
    Navigator.pop(context, note);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.existingNote == null ? '新建笔记' : '编辑笔记'),
        backgroundColor: Colors.white,
        foregroundColor: Colors.black,
        elevation: 0,
        actions: [IconButton(icon: const Icon(Icons.save), onPressed: _saveNote)],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: _titleController,
              decoration: const InputDecoration(
                hintText: '标题',
                border: InputBorder.none,
                focusedBorder: InputBorder.none,
              ),
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
              maxLines: 1,
            ),
            const Divider(height: 24),
            Expanded(
              child: TextField(
                controller: _contentController,
                decoration: const InputDecoration(
                  hintText: '写下你的想法...',
                  border: InputBorder.none,
                  focusedBorder: InputBorder.none,
                ),
                maxLines: null,
                keyboardType: TextInputType.multiline,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ==================== 主界面 ====================
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<Note> _notes = [];

  void _onAddNote() {
    Navigator.push(context, MaterialPageRoute(builder: (_) => const NoteEditorPage()))
        .then((result) {
      if (result != null && result is Note) {
        setState(() => _notes.insert(0, result));
      }
    });
  }

  void _onEditNote(Note note) {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (_) => NoteEditorPage(existingNote: note)),
    ).then((result) {
      if (result != null && result is Note) {
        setState(() {
          final index = _notes.indexWhere((n) => n.id == result.id);
          if (index != -1) _notes[index] = result;
        });
      }
    });
  }

  String _formatTime(DateTime time) {
    return '${time.year}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} '
        '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
  }

  Widget _buildNoteCard(Note note) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(note.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey),
                maxLines: 1, overflow: TextOverflow.ellipsis),
            const SizedBox(height: 8),
            if (note.content.isNotEmpty)
              Text(note.content, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.grey)),
            const SizedBox(height: 12),
            Text(_formatTime(note.createdAt), style: const TextStyle(color: Colors.grey, fontSize: 12)),
          ],
        ),
      ),
    );
  }

  Widget _buildNoteItem(Note note, int index) {
    return Dismissible(
      key: Key(note.id),
      direction: DismissDirection.endToStart,
      background: Container(
        color: Colors.red,
        alignment: Alignment.centerRight,
        padding: const EdgeInsets.only(right: 20),
        child: const Icon(Icons.delete, color: Colors.white),
      ),
      onDismissed: (direction) {
        setState(() => _notes.removeAt(index));
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已删除 "${note.title}"')));
      },
      child: GestureDetector(
        onTap: () => _onEditNote(note),
        child: _buildNoteCard(note),
      ),
    );
  }

  Widget _buildNoteList() {
    if (_notes.isEmpty) {
      return const Center(child: Text('暂无笔记', style: TextStyle(color: Colors.grey)));
    }
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: _notes.length,
      itemBuilder: (context, index) => _buildNoteItem(_notes[index], index),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('我的笔记', style: TextStyle(fontWeight: FontWeight.w500)),
        backgroundColor: Colors.white,
        foregroundColor: Colors.black,
        elevation: 0,
      ),
      body: _buildNoteList(),
      floatingActionButton: FloatingActionButton(onPressed: _onAddNote, child: const Icon(Icons.add)),
    );
  }
}

// ==================== 主程序入口 ====================
void main() {
  runApp(MaterialApp(
    debugShowCheckedModeBanner: false,
    useMaterial3: true, // 启用 Material 3(圆角等)
    home: HomePage(),
  ));
}

运行界面

结语

本文成功构建了一个 零依赖、纯内存、全功能 CRUD 的鸿蒙风格记事本。通过 StatefulWidget 管理状态、Dismissible 实现滑动删除、Navigator 完成页面跳转,我们验证了 Flutter 在 OpenHarmony 平台上构建简洁高效应用的能力。

此内存版虽不具备持久化能力,却是后续进阶的完美起点。在下一篇中,我们将引入 Drift(Moor),将数据安全存储至设备本地,实现真正的"永不丢失"。

相关推荐
renke33645 小时前
Flutter for OpenHarmony:光影迷宫 - 基于局部可见性的沉浸式探索游戏设计
flutter·游戏
晚霞的不甘5 小时前
Flutter for OpenHarmony实现 RSA 加密:从数学原理到可视化演示
人工智能·flutter·计算机视觉·开源·视觉检测
听麟5 小时前
HarmonyOS 6.0+ PC端虚拟仿真训练系统开发实战:3D引擎集成与交互联动落地
笔记·深度学习·3d·华为·交互·harmonyos
江湖有缘5 小时前
基于华为openEuler系统部署Gitblit服务器
运维·服务器·华为
子春一5 小时前
Flutter for OpenHarmony:跨平台虚拟标尺实现指南 - 从屏幕测量原理到完整开发实践
flutter
renke33645 小时前
Flutter for OpenHarmony:形状拼图 - 基于路径匹配与空间推理的交互式几何认知系统
flutter
千逐685 小时前
多物理场耦合气象可视化引擎:基于 Flutter for OpenHarmony 的实时风-湿-压交互流体系统
flutter·microsoft·交互
ujainu5 小时前
保护你的秘密:Flutter + OpenHarmony 鸿蒙记事本添加笔记加密功能(五)
flutter·openharmony
特立独行的猫a5 小时前
主要跨端开发框架对比:Flutter、RN、KMP、Uniapp、Cordova,谁是未来主流?
flutter·uni-app·uniapp·rn·kmp·kuikly