Code Blocks
Fenced code blocks render out of the box with a language label and copy button. Syntax highlighting is not applied by default — wire your own highlighter through codeBuilder. The closed flag tells you whether the closing fence has arrived, which is essential for a smooth streaming experience.
Default behaviour
Without a codeBuilder the package renders the language label, a copy button (calls onCodeCopy when used), and the raw code in a monospace font. No token colouring is applied — add it yourself via codeBuilder.
1// The default renderer shows the language label, a copy button,
2// and the code in a monospace font. No syntax highlighting is applied
3// by default — add it through codeBuilder.
4GptMarkdown(r'''
5```dart
6void main() {
7 print('Hello, world!');
8}
9```
10''')codeBuilder signature
The callback receives the language name, the raw code string, and the closed flag. Return any widget.
1// codeBuilder signature (typedef CodeBlockBuilder):
2//
3// Widget Function(
4// BuildContext context,
5// String name, // language identifier: "dart", "python", "" if none
6// String code, // raw code string between the fences
7// bool closed, // true once the closing \`\`\` has arrived
8// )
9
10GptMarkdown(
11 reply,
12 codeBuilder: (context, name, code, closed) {
13 return MyCodeBlock(language: name, code: code, isClosed: closed);
14 },
15)Adding syntax highlighting
Use any Flutter syntax highlighting package — flutter_highlight, highlight_flutter, or re_highlight. The package makes no assumptions about which one you choose.
1// Add syntax highlighting with flutter_highlight or highlight_flutter.
2// codeBuilder lets you own the entire widget — the package stays
3// dependency-free from any highlighter.
4import 'package:flutter_highlight/flutter_highlight.dart';
5import 'package:flutter_highlight/themes/github.dart';
6
7GptMarkdown(
8 reply,
9 codeBuilder: (context, name, code, closed) {
10 return HighlightView(
11 code,
12 language: name.isEmpty ? 'plaintext' : name,
13 theme: githubTheme,
14 padding: const EdgeInsets.all(12),
15 textStyle: const TextStyle(fontFamily: 'monospace', fontSize: 13),
16 );
17 },
18)The closed flag
When an LLM streams output, the closing ``` fence may not have arrived yet. closed: false means the code is still being written. Use a lighter rendering path during streaming and switch to the full highlighted view once the block is complete.
1// closed = false while the model is still streaming the code block.
2// The closing ``` fence has not arrived yet.
3//
4// Use it to show a simpler UI during streaming and switch to the
5// full highlighted view once the block is complete.
6codeBuilder: (context, name, code, closed) {
7 if (!closed) {
8 // Partial code — show plain text to avoid repeated re-highlighting.
9 return Container(
10 width: double.infinity,
11 padding: const EdgeInsets.all(12),
12 decoration: BoxDecoration(
13 color: Colors.grey.shade900,
14 borderRadius: BorderRadius.circular(8),
15 ),
16 child: Text(
17 code,
18 style: const TextStyle(
19 fontFamily: 'monospace',
20 color: Colors.white70,
21 fontSize: 13,
22 ),
23 ),
24 );
25 }
26 // Full highlighted view once the fence is closed.
27 return MyHighlightedCodeBlock(language: name, code: code);
28}Width and horizontal scroll
Code blocks fill available width by default. Long lines do not wrap — add a horizontal SingleChildScrollView inside your builder if lines may be wider than the screen.
1// Code blocks expand to fill available width by default.
2// Use Container or SizedBox to constrain if needed.
3codeBuilder: (context, name, code, closed) {
4 return Container(
5 width: double.infinity, // fills the column
6 decoration: BoxDecoration(
7 color: const Color(0xFF1E1E1E),
8 borderRadius: BorderRadius.circular(8),
9 ),
10 padding: const EdgeInsets.all(16),
11 child: SingleChildScrollView(
12 scrollDirection: Axis.horizontal, // scroll wide lines
13 child: Text(
14 code,
15 style: const TextStyle(fontFamily: 'monospace', color: Colors.white),
16 ),
17 ),
18 );
19}Selection caveats
SelectableText inside a codeBuilder works once the block is settled (closed: true). While closed: false, the tail is rebuilt every frame, making it an unstable selection target. Selection returns the moment the reply settles.
1// SelectableText inside a codeBuilder works for completed blocks.
2// While closed = false (streaming), the widget rebuilds every frame,
3// so selection is unstable. Selection returns once closed = true.
4codeBuilder: (context, name, code, closed) {
5 return Container(
6 width: double.infinity,
7 padding: const EdgeInsets.all(12),
8 color: Colors.grey.shade900,
9 child: closed
10 ? SelectableText(
11 code,
12 style: const TextStyle(fontFamily: 'monospace', color: Colors.white),
13 )
14 : Text(
15 code,
16 style: const TextStyle(fontFamily: 'monospace', color: Colors.white70),
17 ),
18 );
19}