Flutter:展示大段格式化文本的挑战

Flutter 提供了 RichText 组件,专门用于展示具有多种样式的复合文本。此外,还有一个 Text.rich 构造函数,它们在功能上殊途同归,但在细节处理上略有不同。

这是来自 Flutter 官方文档的一个经典示例,展示了如何构建一个具有多种样式的复合文本。

显然(对我而言),使用这些组件来展示大段(超过 5 行)文本需要耗费大量精力,而且会导致代码极难维护。

可选的替代方案有:

  1. HTML
  2. Markdown

我们可以使用 HTML 或 Markdown 来格式化文件,将其存储在 assets 资源目录中,然后调用合适的 Flutter 软件包进行展示。

HTML 还是 Markdown? 我认为默认选 Markdown,因为用它来格式化文本文件要简单得多。

有趣的是,根据名字来看,VS Code 排名最靠前的前两(可能前三)个 Markdown 扩展插件都是中国人写的。Markdown 似乎对中国人有着某种特殊的吸引力。致敬。我使用的是这一个:

如果你手头已经有现成的 HTML 文件,或者需要实现一些 Markdown 无法完成的特殊格式,那么 HTML 也是一个合理的选择。

举个典型的 HTML 场景:我有一个旧项目,里面的文本内嵌了 <audio> 标签。关于这一点,我会在另一篇文章中详细展示。

那么,到底该用哪个插件来把 Markdown 转换成 Flutter 组件呢?

我搜到了 8 个,以下是排名前 5 的:

  1. flutter_markdown(官方出品,已停止维护)
  2. flutter_markdown_plus(100 赞,15万次下载)
  3. gpt_markdown(276 赞,5.7万次下载)
  4. markdown_widget(406 赞,7500次下载)
  5. flutter_md(46 赞,3300次下载)

综合考虑点赞数、下载量以及最直观的使用示例,我选择了 gpt_markdown。不过我想,其他的应该也都能用。话说回来,为什么我们需要 8 个这么多?

  • markdown_widget 的特色是支持 TOC(目录生成)
  • flutter_md 则承诺了更好的性能,因为它跳过了 HTML 转换步骤,直接将 Markdown 渲染为 Flutter 原生组件。

总之,我不打算对这些插件进行深度横评。我的需求非常简单:我只有一段格式化好的文本,想把它显示出来就行。

以下是文本的一部分:

markdwon 复制代码
### Lorem ipsum  
  
#### Mauris congue  
  
Lorem ipsum dolor sit amet, consectetur adipiscing **elit**. Mauris congue metus et dui cursus, ut pulvinar tellus porta. Proin sit amet orci laoreet, mollis nunc nec, porta orci. Suspendisse potenti. Ut *efficitur facilisis urna*, id tempus ligula rutrum ut. Ut nec tempor odio, vitae rutrum ex. Morbi placerat sagittis fringilla. Sed magna orci, venenatis in commodo quis, accumsan non nulla. Duis in lacus tortor. Proin a euismod est, sit amet auctor enim.

正如我们所见,这段文本包含了一些基础的格式化内容。

我在 ViewModel (这里用的是 GetxController)中读取了它:

dart 复制代码
void onInit() {
  super.onInit();
  loadMd();
}

Future<void> loadMd() async {
  try {
    mdContent = await rootBundle.loadString('assets/markdown/content.md');
    update();
  } catch (e) {
    print(e);
  }
}

然后在页面中展示一下:

dart 复制代码
return SingleChildScrollView(
  padding: EdgeInsets.all(16),
  child: GptMarkdown(
    controller.mdContent,
    style: TextStyle(
      fontSize: 16,
    ),
  ),
);

很简单。

显然,我们需要创建一个 assets/markdown 文件夹,放入 content.md 文件,并在 pubspec.yaml 中声明该文件夹。

不幸的是,当第一次打开该视图时,页面过渡并不平滑,通过性能分析(Profile)可以发现明显的卡顿(Jank)。

据我理解,问题出在读取文件这一环节。在第一次读取之后,rootBundle 会缓存内容,所以后续打开就会快很多。

因此,解决方案可以是在打开视图之前就预加载内容。 由于展示 Markdown 的视图是从主页(Home View)打开的,我们只需要在主页显示之后立即读取文件即可。

首先,我创建了一个独立的数据源类(DataSource)

dart 复制代码
class MdDatasource {
  static final MdDatasource _instance = MdDatasource._();
  String? _mdContent;

  MdDatasource._();

  factory MdDatasource() => _instance;

  Future<String> loadMdContent() async {
    if (_mdContent!= null) {
      return _mdContent!;
    }

    try {
      _mdContent= await rootBundle.loadString('assets/markdown/content.md');
      return _mdContent!;
    } catch (e) {
      _mdContent = 'Something wrong'.tr;
      return _mdContent!;
    }
  }
}

并在 HomeControlleronReady 方法中调用它:

dart 复制代码
void onReady() {
  super.onReady();
  MdDatasource().loadMdContent();
}

GetX 的 onReady 方法是通过 addPostFrameCallback 实现的,因此它会在 HomeView 完全显示之后才被调用。

尽管寄予厚望,但这并没有解决卡顿(Jank)问题。看来导致卡顿的元凶并不是读取文件,而是 Markdown 的解析与渲染过程

由于 flutter_md 软件包承诺提供更好的性能,让我们尝试用它来替换 gpt_markdown

首先,我重写了 MdDatasource

dart 复制代码
class MdDatasource {
  static final MdDatasource _instance = MdDatasource._();
  MdDatasource._();
  factory MdDatasource() => _instance;

  Markdown? _markdown;


  Future<Markdown?> loadMarkdown() async {
    if (_markdown != null) {
      return _markdown!;
    }

    try {
      var content = await rootBundle.loadString('assets/markdown/content.md');
      _markdown =  Markdown.fromString(content);
      return _markdown!;
    } catch (e) {
      print(e);
    }

    return null;
  }
}

Gemini said

我们不再仅仅是"预加载"文件内容,而是利用该软件包中的 Markdown 类对内容进行"预解析"。

HomeController 中:

dart 复制代码
void onReady() {
  super.onReady();
  MdDatasource().loadMarkdown();
}

然后在 MdController 中调用它:

dart 复制代码
Future<void> loadMarkdown() async {
  markdown = await MdDatasource().loadMarkdown();
  update();
}

最后,在 MdView 中将其展示出来:

dart 复制代码
MarkdownWidget(
  markdown: controller.markdown!,
  theme: MarkdownThemeData(
    textStyle: TextStyle(
      fontSize: 16,
      color: colorScheme.onSurface,
    ),
  ),
),

过渡效果终于变得视觉平滑了。看来,人们不断"造轮子(重新发明轮子)"也不全是坏事,至少在追求极致性能的路上,我们有了更多选择。

尽管如此,目前仍会偶尔出现一帧掉帧,且诱因并不固定:有时是 Raster 线程(栅格化线程) ,有时则是 UI 线程(布局阶段) 。Flutter 的性能分析(Profiling)既是一门科学,也是一门艺术,要完全精通它确实还有很长的路要走。


🏁 总结回顾

要在 Flutter 中优雅且高性能地展示大段格式化文本,最佳实践路径如下:

  1. 资源管理 :在 assets 中创建 Markdown (.md) 文件,方便维护与内容解耦。

  2. 插件选择 :使用 flutter_md 软件包进行渲染,它在原生转换效率上表现出色。

  3. 核心优化策略(最关键)

    • 不要在跳转时加载:避免在页面切换动画期间进行 I/O 操作。
    • 预解析 (Pre-parsing) :在主页(HomeView)显示后的空闲时间,提前将字符串解析为 Widget 树,实现"秒开"体验。

感谢阅读!希望这些实战经验能帮你绕过 Flutter 长文本渲染的那些坑。

相关推荐
兆子龙2 小时前
Node.js ESM Loader Hooks 介绍:用 module.register 做转译、Import Map 与自定义解析
前端
四眼肥鱼2 小时前
flutter 利用flutter_libserialport 实现SQ800 串口通信
前端·flutter
ZFSS2 小时前
OpenAI Images Edits API 申请及使用
前端·人工智能
Lee川2 小时前
从零构建AI对话应用:Vite脚手架搭建与API密钥安全实践
前端·程序员
允许部分打工人先富起来2 小时前
在node项目中执行python脚本
前端·python·node.js
钟智强2 小时前
Flutter引擎Android平台JNI层未验证指针转换漏洞
前端
骑着小黑马2 小时前
Electron + Vue3 + AI 做了一个新闻生成器:从 0 到 1 的完整实战记录
前端·人工智能
Sailing2 小时前
LLM 调用从 60s 卡死降到 3s!彻底绕过 tiktoken 网络阻塞(LangChain.js 必看)
前端·langchain·llm
洋洋技术笔记2 小时前
计算属性与侦听器
前端·vue.js