
前言
对于注重内容创作的备忘录和日记应用,纯文本输入框的功能太过单薄。用户希望能用 Markdown 语法写标题、列表、引用------同时又能在发布前预览渲染效果。这种"写 + 预览"的双模式体验,在 Notion、Obsidian 等工具中已是标配。
Flutter 生态中,flutter_markdown 包可以直接在 widget 树中渲染 Markdown 文本,无需 WebView。配合一个简单的状态切换,就能实现编辑态和预览态的无缝切换。
本文将展示鸿蒙 Flutter 备忘录应用中 Markdown 编辑/预览功能的完整实现。
项目仓库:todo_flutter_harmony
依赖配置
在 pubspec.yaml 中添加:
yaml
dependencies:
flutter_markdown: ^0.7.7+1
flutter_markdown 是一个纯 Dart 实现的 Markdown 解析和渲染库,不依赖任何原生插件,因此与鸿蒙 OHOS 完全兼容。
状态设计
编辑/预览切换的核心状态只有一个:
dart
enum EditorMode { edit, preview }
// 在 State 中
EditorMode _mode = EditorMode.edit;
UI 布局
编辑页的 AppBar 右侧放置一个切换按钮,内容区根据 _mode 切换显示 TextField 或 MarkdownBody:
dart
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_mode == EditorMode.edit ? '编辑' : '预览'),
actions: [
// 切换按钮
IconButton(
icon: Icon(
_mode == EditorMode.edit
? Icons.visibility_outlined
: Icons.edit_outlined,
),
tooltip: _mode == EditorMode.edit ? '预览' : '编辑',
onPressed: _toggleMode,
),
IconButton(
icon: const Icon(Icons.check),
onPressed: _save,
),
],
),
body: _mode == EditorMode.edit ? _buildEditor() : _buildPreview(),
);
}
编辑器实现
dart
Widget _buildEditor() {
return Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _textController,
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
decoration: const InputDecoration(
hintText: '支持 Markdown 语法...\n\n# 标题\n**粗体** *斜体*\n- 列表项',
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
style: const TextStyle(
fontSize: 16,
height: 1.6,
),
),
);
}
关键设置:
maxLines: null+expands: true:让文本域撑满可用空间textAlignVertical: TextAlignVertical.top:光标和文字从顶部开始contentPadding: EdgeInsets.zero:去除默认内边距,与预览态对齐
预览实现
dart
Widget _buildPreview() {
final content = _textController.text;
if (content.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.article_outlined, size: 64, color: Colors.grey.shade300),
const SizedBox(height: 12),
Text('暂无内容', style: TextStyle(color: Colors.grey.shade400)),
],
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: MarkdownBody(
data: content,
selectable: true,
styleSheet: _markdownStyleSheet,
),
);
}
自定义 Markdown 样式
flutter_markdown 提供了 MarkdownStyleSheet 用于定制渲染样式。让它与应用的整体主题保持一致:
dart
MarkdownStyleSheet get _markdownStyleSheet {
return MarkdownStyleSheet(
// H1 - H6 标题样式
h1: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, height: 1.4),
h2: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, height: 1.4),
h3: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, height: 1.3),
// 正文字体
p: const TextStyle(fontSize: 16, height: 1.8, color: Color(0xFF333333)),
// 代码块
code: TextStyle(
backgroundColor: Colors.grey.shade100,
fontSize: 14,
fontFamily: 'monospace',
),
codeblockDecoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
),
// 引用块
blockquoteDecoration: BoxDecoration(
color: const Color(0xFF4DB6AC).withOpacity(0.05),
border: const Border(
left: BorderSide(color: Color(0xFF4DB6AC), width: 3),
),
),
// 列表缩进
listIndent: 20,
// 水平分割线
horizontalRuleDecoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey.shade300, width: 0.5),
),
),
);
}
切换过渡动画
从编辑态切换到预览态时,简单的"闪现"体验不够好。加入一个交叉淡入淡出:
dart
void _toggleMode() {
setState(() {
_mode = _mode == EditorMode.edit
? EditorMode.preview
: EditorMode.edit;
});
}
@override
Widget build(BuildContext context) {
// ...
body: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
child: _mode == EditorMode.edit
? _buildEditor()
: _buildPreview(),
),
}
注意:当 AnimatedSwitcher 的两个子 widget 类型不同时,它会自动应用默认的淡入淡出过渡。如果两个子 widget 类型相同(比如都是 Padding),需要显式指定 key 来区分。
自动保存 + 草稿恢复
切换到预览模式时自动保存草稿,避免意外退出导致内容丢失:
dart
void _toggleMode() {
if (_mode == EditorMode.edit) {
// 编辑 → 预览:保存草稿
_saveDraft();
}
setState(() {
_mode = _mode == EditorMode.edit
? EditorMode.preview
: EditorMode.edit;
});
}
void _saveDraft() {
// 用 shared_preferences 或文件存储草稿
final file = File('${storagePath}/.memo_draft.txt');
file.writeAsStringSync(_textController.text);
}
编辑页与列表页的数据流
dart
// 列表页跳转到编辑页
Navigator.pushNamed(
context,
'/diary/new',
).then((_) {
// 返回后刷新列表
if (context.mounted) {
context.read<DiaryProvider>().loadDiaries();
}
});
// 编辑页保存
void _save() {
final diary = Diary(
content: _textController.text,
mood: _selectedMood,
createdAt: DateTime.now(),
);
context.read<DiaryProvider>().addDiary(diary);
Navigator.pop(context);
}
鸿蒙兼容性
flutter_markdown 是纯 Dart 实现:
- 词法分析和 AST 生成:Dart 层
- Widget 构建:Flutter 框架层
零原生依赖,在 Android、iOS、鸿蒙 OHOS 上完全一致。这也是选择 flutter_markdown 而非 WebView 方案的关键原因------WebView 在鸿蒙上的行为可能与 Android 不同,引入了不确定性。
进阶:快捷工具栏
除了纯手动输入 Markdown 语法,可以加一个快捷工具栏帮助用户快速插入语法:
dart
Widget _buildToolbar() {
return Container(
height: 44,
decoration: BoxDecoration(
color: Colors.grey.shade50,
border: Border(top: BorderSide(color: Colors.grey.shade200)),
),
child: Row(
children: [
_ToolbarButton(icon: Icons.title, label: 'H2', onTap: () => _insertSyntax('## ', '')),
_ToolbarButton(icon: Icons.format_bold, label: 'B', onTap: () => _insertSyntax('**', '**')),
_ToolbarButton(icon: Icons.format_italic, label: 'I', onTap: () => _insertSyntax('*', '*')),
_ToolbarButton(icon: Icons.list, label: '-', onTap: () => _insertSyntax('- ', '')),
_ToolbarButton(icon: Icons.code, label: '`', onTap: () => _insertSyntax('`', '`')),
_ToolbarButton(icon: Icons.link, label: '[]()', onTap: () => _insertSyntax('[', '](url)')),
],
),
);
}
void _insertSyntax(String before, String after) {
final text = _textController.text;
final selection = _textController.selection;
final selectedText = selection.textInside(text);
final newText = text.replaceRange(
selection.start,
selection.end,
'$before$selectedText$after',
);
_textController.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: selection.start + before.length + selectedText.length,
),
);
}
总结
Markdown 编辑预览功能的实现只需要三步:
- 引入
flutter_markdown:纯 Dart,零原生依赖,鸿蒙完美兼容 - 状态切换 :一个
EditorMode枚举控制 TextField ↔ MarkdownBody 的互斥显示 - 样式定制 :
MarkdownStyleSheet让渲染效果与应用主题保持一致
配合 AnimatedSwitcher 的淡入淡出过渡,编辑和预览之间的切换体验流畅自然。
完整项目代码见:todo_flutter_harmony