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.

default_code_block.dart
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.

code_builder.dart
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.

syntax_highlighting.dart
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.

closed_flag.dart
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.

wide_code.dart
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.

selection.dart
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}