Flutter中如何实现类似pub.dev的带复制功能的代码容器组件?
Flutter实现类pub.dev带复制功能的代码视图组件

实现该组件不需要复杂的原生依赖,核心由语法高亮、代码块容器、复制交互三个模块组成,以下是可直接复用的实现方案:
第一步:添加依赖
在pubspec.yaml中添加语法高亮相关依赖,复制功能直接使用Flutter内置的剪贴板服务即可,无需额外插件:
dependencies: flutter: sdk: flutter flutter_highlighter: ^0.1.1 # 可替换为pub上最新稳定版本
第二步:封装可复用代码块组件
组件支持传入代码内容、语言类型、可选文件名,交互和样式完全对齐pub.dev:带圆角的浅灰背景、长代码横向滚动、右上角复制按钮点击后临时显示「已复制」状态,2秒后自动恢复。
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_highlighter/flutter_highlighter.dart'; import 'package:flutter_highlighter/themes/github.dart'; class CodeViewBlock extends StatefulWidget { final String codeContent; final String language; final String? fileName; const CodeViewBlock({ super.key, required this.codeContent, required this.language, this.fileName, }); @override State<CodeViewBlock> createState() => _CodeViewBlockState(); } class _CodeViewBlockState extends State<CodeViewBlock> { bool _copied = false; @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: const Color(0xFFF6F8FA), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFFD0D7DE)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ // 顶部文件名栏,传入fileName时显示 if (widget.fileName != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( widget.fileName!, style: const TextStyle( fontSize: 13, color: Color(0xFF656D76), fontWeight: FontWeight.w500, ), ), _buildCopyButton(), ], ), ), // 代码渲染区域 Stack( children: [ SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.all(16), child: HighlightView( widget.codeContent, language: widget.language, theme: githubTheme, padding: EdgeInsets.zero, textStyle: const TextStyle( fontFamily: 'monospace', fontSize: 13, height: 1.5, ), ), ), // 无文件名时复制按钮悬浮在右上角 if (widget.fileName == null) Positioned( top: 8, right: 8, child: _buildCopyButton(), ), ], ), ], ), ); } Widget _buildCopyButton() { return Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(4), onTap: () async { await Clipboard.setData(ClipboardData(text: widget.codeContent)); setState(() => _copied = true); Future.delayed(const Duration(seconds: 2), () { if (mounted) setState(() => _copied = false); }); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), border: Border.all(color: const Color(0xFFD0D7DE)), color: Colors.white, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _copied ? Icons.check : Icons.copy_rounded, size: 14, color: _copied ? Colors.green : const Color(0xFF656D76), ), const SizedBox(width: 4), Text( _copied ? '已复制' : '复制', style: TextStyle( fontSize: 12, color: _copied ? Colors.green : const Color(0xFF656D76), ), ), ], ), ), ), ); } }
第三步:组件调用
直接在页面中传入对应参数即可使用:
CodeViewBlock( codeContent: ''' void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue), home: const HomePage(), ); } } ''', language: 'dart', fileName: 'main.dart', )
自定义调整说明
- 默认使用的GitHub高亮主题和pub.dev风格高度匹配,如果需要更精准的配色,可以自行修改
githubTheme中对应语法token的颜色值 - 如果需要支持代码行号,可以提前将代码按换行符拆分,在每行代码前渲染对应行号文本,和代码行做基线对齐即可
- 长代码需要纵向滚动时,在横向滚动的
SingleChildScrollView外层再嵌套一层纵向的SingleChildScrollView即可 - 可以替换代码字体为JetBrains Mono、Roboto Mono等更适合代码展示的等宽字体,提升显示效果
内容的提问来源于stack exchange,提问作者Ravi Sevta
相关产品推荐
相关产品推荐

