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

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

显然(对我而言),使用这些组件来展示大段(超过 5 行)文本需要耗费大量精力,而且会导致代码极难维护。
可选的替代方案有:
- HTML
- Markdown
我们可以使用 HTML 或 Markdown 来格式化文件,将其存储在 assets 资源目录中,然后调用合适的 Flutter 软件包进行展示。
HTML 还是 Markdown? 我认为默认选 Markdown,因为用它来格式化文本文件要简单得多。
有趣的是,根据名字来看,VS Code 排名最靠前的前两(可能前三)个 Markdown 扩展插件都是中国人写的。Markdown 似乎对中国人有着某种特殊的吸引力。致敬。我使用的是这一个:

如果你手头已经有现成的 HTML 文件,或者需要实现一些 Markdown 无法完成的特殊格式,那么 HTML 也是一个合理的选择。
举个典型的 HTML 场景:我有一个旧项目,里面的文本内嵌了 <audio> 标签。关于这一点,我会在另一篇文章中详细展示。
那么,到底该用哪个插件来把 Markdown 转换成 Flutter 组件呢?
我搜到了 8 个,以下是排名前 5 的:
- flutter_markdown(官方出品,已停止维护)
- flutter_markdown_plus(100 赞,15万次下载)
- gpt_markdown(276 赞,5.7万次下载)
- markdown_widget(406 赞,7500次下载)
- 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!;
}
}
}
并在 HomeController 的 onReady 方法中调用它:
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 中优雅且高性能地展示大段格式化文本,最佳实践路径如下:
-
资源管理 :在
assets中创建 Markdown (.md) 文件,方便维护与内容解耦。 -
插件选择 :使用
flutter_md软件包进行渲染,它在原生转换效率上表现出色。 -
核心优化策略(最关键) :
- 不要在跳转时加载:避免在页面切换动画期间进行 I/O 操作。
- 预解析 (Pre-parsing) :在主页(HomeView)显示后的空闲时间,提前将字符串解析为 Widget 树,实现"秒开"体验。
感谢阅读!希望这些实战经验能帮你绕过 Flutter 长文本渲染的那些坑。