You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 08:09:14