Streaming Messages
StreamingMessageView renders a reply as the agent writes it: Markdown, code, tables, images and charts, typed out letter by letter, as ChatGPT does. AIConversationView shows every reply with it, and you can use it on its own in a widget of yours:
StreamingMessageView(
text: reply.text, // updated in real time as chunks arrive
expectMoreText: reply.isGenerating, // true while the reply is being generated
);Type the reply out
While expectMoreText is true, that is while the backend may still send more of the reply, the view types out the text that's new since the last update. When the text is replaced by something other than a continuation, the view keeps the part it shares with what is on screen and types only the rest. The update that turns expectMoreText to false is still typed out to the end, so a reply doesn't jump when its last chunk arrives. Only text that arrives after that shows in full at once, as for a reply loaded from history. That's why a stored reply doesn't type itself out again when the user scrolls back to it.
typingSpeed sets the speed of the typing, as the time per character. The default is 5 ms. onTypewriterStateChanged reports the typewriter's TypewriterState: typing, idle, paused or stopped.
StreamingMessageView(
text: markdownText,
expectMoreText: isGenerating,
typingSpeed: const Duration(milliseconds: 5),
onTypewriterStateChanged: (state) {
// TypewriterState.typing | idle | paused | stopped
},
);The typewriter usually falls behind the backend, so the reply is still being revealed after generation ends. To know when it's completely on screen, use onFinished. It's called once expectMoreText is false and every character has been revealed:
StreamingMessageView(
text: markdownText,
expectMoreText: isGenerating,
onFinished: () => setState(() => showActions = true),
);With reduced motion turned on in the system settings, the view shows each new text in full instead of typing it out.
With Stream Chat, a reply is generating while its message's generating custom field is true, which you read as message.extraData['generating'] == true. What your backend sends covers how to set it.
By default, the view renders the revealed text as Markdown. To put the typewriter effect on a layout of your own, drive it with a TypewriterController and a TypewriterBuilder:
final controller = TypewriterController(text: '');
// As new chunks arrive from the LLM:
controller.updateText(accumulatedText);
// In your widget tree:
TypewriterBuilder(
controller: controller,
builder: (context, value, child) => Text(value.text),
);If you want markdown without the animation, use AIMarkdownBody, the renderer that StreamingMessageView is built on:
AIMarkdownBody(
data: markdownText,
selectable: true, // enables text selection on desktop and web
onTapLink: (text, href, title) {
// Handle the link tap.
},
);StreamingMessageView takes the same onTapLink. Links are ignored without it, and opening one needs a launcher of your choice, such as url_launcher.
stream_chat_flutter 10.5.0 deprecated its AI widgets, which moved to stream_chat_flutter_ai: StreamingMessageView, AITypingIndicatorView, AnimatedDots, StreamTypewriterBuilder, TypewriterController, TypewriterValue, TypewriterState and TypewriterWidgetBuilder. To move over, add stream_chat_flutter_ai to your app, as described in Install the SDKs, and hide the old names when you import stream_chat_flutter, as shown in Stream Chat integration. Then rename StreamTypewriterBuilder to TypewriterBuilder, and pass expectMoreText to StreamingMessageView, from the message's generating field, since the view types text out only while it's true. Drop AnimatedDots and AITypingIndicatorView.dotCount: the indicator always shows three dots, styled by dotColor, dotSize and dotSpacing. The typewriter also types faster by default, 5 ms per character instead of 10 ms, and the dots are smaller, 5 pixels instead of 8. Pass typingSpeed and dotSize to keep the old look.
Code and charts
Code fences are drawn with a CodeBlockView: a framed code block with a header with the code's language and a button that copies the code. A code fence whose language is json, chart, chartjs, echarts, highcharts, plotly or vega, and whose content is a chart spec, renders as a native chart with a ChartView instead. A fence is styled from its opening line onwards, so a block that is still streaming never shows the raw fence markers. A chart fence shows as code until its content reads as a chart spec.
Charts are drawn in Flutter with fl_chart, so they need no network. fl_chart and the Markdown renderer, flutter_markdown_plus, are dependencies of stream_chat_flutter_ai, so you don't add them to your app yourself. Syntax highlighting and LaTeX formulas each need a library that you add yourself, such as re_highlight and flutter_math_fork, or any other, since the package only asks for a callback. Without them, the components still work, but code and formulas render as plain text:
dependencies:
stream_chat_flutter_ai: ^0.2.0
re_highlight: ^0.0.3 # code highlighting
flutter_math_fork: ^0.7.4 # math renderingCharts
The charts read USpec, Chart.js, ECharts, Highcharts, Plotly heatmaps and (a subset of) Vega-Lite payloads. USpec is a simpler format of the package's own, which is handy to describe in your agent's instructions:
```chart
{
"kind": "bar",
"title": "Signups per month",
"xLabel": "Month",
"yLabel": "Users",
"series": [
{
"name": "2026",
"points": [{ "x": "Jan", "y": 120 }, { "x": "Feb", "y": 180 }, { "x": "Mar", "y": 240 }]
}
]
}
```ChartView renders a USpec as a line, area, bar (vertical or horizontal, grouped or stacked), single stacked bar, scatter, bubble, pie (full, donut or semicircle), radar, radial, histogram or heatmap chart. StreamingMessageView already renders chart fences for you. AIMarkdownBody takes a chartLanguages set to change the languages of chart fences, or an empty set to turn charts off. Use ChartView directly when you parse the model output yourself, with USpecParser:
final spec = USpecParser.tryParse(jsonString);
if (spec != null) ChartView(spec: spec);Pies, radial charts and single stacked bars, and any other chart except a heatmap or histogram that plots two or more series, get a legend below the plot. Pie slices big enough for it (8% by default, piePercentageThreshold) are labeled with their percentage. Turn the legend off or move it above the plot, and the percentages off, with the chart theme:
ChartView(
spec: spec,
theme: const ChartThemeData(
legendPosition: ChartLegendPosition.top,
showPiePercentages: false,
),
);Each chart describes itself to screen readers, for example "Bar chart, Messages per day, 5 categories, values 8 to 24". Pass semanticsLabel to replace that sentence, or an empty string to describe the chart yourself.
Syntax highlighting
Code blocks render as plain monospace text by default. Highlighting is opt-in, because a grammar set is several times the size of the whole package. Supply your tokenizer through codeHighlighter:
StreamingMessageView(
text: message,
codeHighlighter: highlightCode,
);AIMarkdownBody takes the same callback as codeHighlighter, and CodeBlockView as highlighter. To highlight the replies of an AIConversationView, which takes no codeHighlighter, register a codeBlock builder with streamAIComponentBuilders:
streamAIComponentBuilders(
codeBlock: (context, props) => CodeBlockView(
code: props.code,
language: props.language,
highlighter: props.highlighter ?? highlightCode,
theme: props.theme,
),
);The callback has a small contract:
TextSpan? highlightCode(CodeHighlightRequest request);The request carries the code, its language, the baseStyle (the block's monospace style), the brightness of the block's background and its resolved theme. language arrives exactly as the fence wrote it, so case folding and alias resolution are yours to handle. Return null for a language you don't cover and the block renders as plain text. A highlighter that throws, or returns spans whose text doesn't match the code, is reported through FlutterError.onError, and the block falls back to plain text as well.
Pick the token colors from request.brightness, not from the app's brightness, since a code block can be dark in a light app. With re_highlight's themes, for example:
final palette = request.brightness == Brightness.dark ? vs2015Theme : atomOneLightTheme;The highlightCode function used on this page is yours to define. The package's showcase app contains a complete implementation built on re_highlight. Every grammar you import adds to your app size, so keep only the languages your users are likely to see. Prefer a top-level function over an inline closure, so the view can tell a new highlighter from a new closure over the same one.
While a fence is streaming, your highlighter isn't called on every frame. It runs each time the code gains 64 characters, plus one final pass once the text stops changing. Blocks over 20,000 characters skip highlighting.
Code block appearance
A code block's frame comes from CodeBlockThemeData: brightness, backgroundColor, headerBackgroundColor, languageLabelStyle, copyIconColor, copiedIconColor and codeTextStyle. By default the block is dark (#1E1E1E with #D4D4D4 text) in light and dark mode alike. Set brightness to Brightness.light for a light frame, and every default color follows it:
StreamingMessageView(
text: message,
codeBlockTheme: const CodeBlockThemeData(brightness: Brightness.light),
);StreamingMessageView and AIMarkdownBody pass their codeBlockTheme to every fence, and CodeBlockView takes it as theme. To set it once for the whole app, use AITheme.codeBlockTheme, as described in Theming.
Math
The package recognizes LaTeX between \(...\) and \[...\], but typesetting is left to you, so it doesn't pull in a TeX renderer. Provide a mathBuilder, and formulas render. Without one, the TeX source is shown as text:
// With `flutter_math_fork` in your pubspec.
StreamingMessageView(
text: markdownText,
mathBuilder: (context, tex, style, {required inline}) => Math.tex(
tex,
textStyle: style,
mathStyle: inline ? MathStyle.text : MathStyle.display,
),
);Pass useDollarDelimitersForMath: true to also accept $...$ and $$...$$. It is off by default, because $ collides with currency in ordinary prose.
To render formulas everywhere at once, including the replies of an AIConversationView, which takes no mathBuilder, register a math builder with streamAIComponentBuilders instead. A mathBuilder given to a widget still wins there:
streamAIComponentBuilders(
math: (context, props) => Math.tex(
props.tex,
textStyle: props.style,
mathStyle: props.inline ? MathStyle.text : MathStyle.display,
),
);Show what the agent is doing
Before the first word of a reply arrives, the agent may spend a while thinking or calling tools. AITypingIndicatorView shows what it's doing: a text of your choice, such as "Thinking" or "Checking sources", followed by three animated dots:
AITypingIndicatorView(
text: 'Checking external sources',
dotColor: Colors.blue, // optional; unset values come from the theme
dotSize: 5,
dotSpacing: 4,
);By default the caption is the Stream theme's bodyDefault in textSecondary, and the dots match its color: three dots of 5 pixels, 4 pixels apart. dotColor, dotSize and dotSpacing are all optional, and textStyle is merged over the themed caption, so passing only a font size keeps the themed color. The indicator is a screen-reader live region labeled with its text, so keep it mounted and change the text to have each new state announced. With reduced motion turned on, the dots don't animate.
An AIConversation tells you what the agent says it's doing as its activity. Without one, Showing the AI indicator shows where the agent says what it's doing. While the agent only thinks, AIConversationView shows "Thinking... 4s" by itself, as described in Show the agent thinking.
Appearance
The view renders Markdown with the colors and text styles of the StreamTheme, and takes a styleSheet to override the markdown's styles. These theme data classes, on AITheme or in their scopes, as described in Theming, style the rest:
CodeBlockThemeData(codeBlockTheme,CodeBlockTheme): the code blocks, as described in Code and charts.ChartThemeData(chartTheme,ChartTheme): the charts, such asseriesColors,height,axisLabelStyle,titleTextStyle,gridLineColor, the heatmap's colors, the legend and the pie percentages.TypingIndicatorThemeData(typingIndicatorTheme,TypingIndicatorTheme):textStyle,dotColor,dotSizeanddotSpacingof everyAITypingIndicatorView.
The code blocks' copy button shows the copy icon of the StreamTheme's icons, and checkmark once the code is copied.
Its texts are members of AITranslations, as described in Localization: copyCode and codeCopied for the code blocks' copy button, unnamedChartSeries, unnamedChartSlice and chartLegendMore for the charts, and chartSemanticsLabel for the sentence a chart is read out as.