鸿蒙Flutter实战:Markdown编辑与预览实时切换

前言

对于注重内容创作的备忘录和日记应用,纯文本输入框的功能太过单薄。用户希望能用 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 切换显示 TextFieldMarkdownBody

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 编辑预览功能的实现只需要三步:

  1. 引入 flutter_markdown:纯 Dart,零原生依赖,鸿蒙完美兼容
  2. 状态切换 :一个 EditorMode 枚举控制 TextField ↔ MarkdownBody 的互斥显示
  3. 样式定制MarkdownStyleSheet 让渲染效果与应用主题保持一致

配合 AnimatedSwitcher 的淡入淡出过渡,编辑和预览之间的切换体验流畅自然。

完整项目代码见:todo_flutter_harmony

相关推荐
不羁的木木15 小时前
ArkUI实战演练04-状态管理与数据驱动
harmonyos
坚果的博客15 小时前
AtomCode 助力开源鸿蒙跨平台三方库生态共建
华为·开源·harmonyos
wechat_Neal15 小时前
华为花瓣地图海外版市场与技术对标分析报告
华为·汽车
lauo15 小时前
从华为“韬(τ)定律”到ibbot手机:AI原生时代的“τ”解法
华为·智能手机·ai-native
GLAB-Mary15 小时前
苏州华为培训哪家好?2026零基础华为HCIA/HCIP/HCIE指南
华为·华为认证·hcie·hcia·hcip
枫叶丹415 小时前
【HarmonyOS 6.0】Live View Kit深度解析:实况胶囊尾部图标的设计哲学与实现全流程
开发语言·华为·harmonyos
小雨青年1 天前
鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 13:顶部导航在窄窗口下如何简化
华为·harmonyos
FrameNotWork1 天前
HarmonyOS 短视频滑动交互实现:打造流畅的上下切换体验
音视频·交互·harmonyos
FrameNotWork1 天前
HarmonyOS 照片浏览器手势交互实现:打造流畅的滑动体验
华为·交互·harmonyos