Message & Thread
Conversation rendering from plain message data — roles, streaming, and the pending and error states.
FlowThread renders a conversation from plain message data — user,
assistant, and system roles — and FlowMessage renders a single turn: an
ink-wash bubble for the user, typeset prose on the page for the assistant
(assistant text renders as markdown by default;
markdown: false opts a literal-text host out). Neither knows where the
messages came from.
A conversation that still fits its viewport reads from the top, the AI-app convention; once it outgrows the viewport the thread anchors to the newest message.
A conversation
Section titled “A conversation”FlowThread( messages: [ FlowMessageData.text( id: '1', role: FlowMessageRole.user, text: 'What is flow_ui?', ), FlowMessageData.text( id: '2', role: FlowMessageRole.assistant, text: 'An open-source Flutter UI library to build production-grade Chat & AI assistant interfaces...', ), FlowMessageData.text( id: '3', role: FlowMessageRole.system, text: 'Model changed to Sonnet', ), ],)Streaming
Section titled “Streaming”Streaming is data, not streams: as chunks arrive, rebuild with the updated
message list and the thread animates the reveal. Status walks
pending → streaming → complete.
FlowThread( messages: [ ...history, FlowMessageData( id: 'reply', role: FlowMessageRole.assistant, parts: [FlowTextPart(streamedSoFar)], status: FlowMessageStatus.streaming, ), ],)Pending and error states
Section titled “Pending and error states”A pending assistant message shows the thinking indicator until the first
token arrives; an error status wraps the content in an error bubble.
// Pending: the thinking indicator until the first token arrives.FlowMessage( FlowMessageData( id: 'p', role: FlowMessageRole.assistant, status: FlowMessageStatus.pending, ), thinkingLabel: 'Thinking…',)
// Error: content in an error bubble.FlowMessage( FlowMessageData.text( id: 'e', role: FlowMessageRole.assistant, text: 'Something went wrong while generating a response.', status: FlowMessageStatus.error, ),)Message parts
Section titled “Message parts”Message content is typed parts, not strings: a sealed FlowMessagePart
with FlowTextPart, FlowAttachmentPart, FlowImagePart, and
FlowCustomPart subtypes (plus FlowCodePart, FlowErrorPart and
FlowConfirmationPart).
FlowAttachmentPart renders sent files — lifted above a user bubble as
image cards, tiles elsewhere —
while FlowImagePart is the large-format picture, an AI-generated image
presented as content: a null image renders the generating state, a
shimmering block at the part’s aspect ratio, and the host re-renders
with the ImageProvider set when the picture lands. Tapping a landed
image opens the full-screen preview.
FlowCustomPart plus a FlowCustomPartBuilder on the message is the
extension seam — it carries a type your builder switches on and an
arbitrary data payload, so the host injects its own content (a tool
card, a chart) without the library knowing what it is.
Selecting text
Section titled “Selecting text”Text in a thread is selectable, the way a chat in a browser is: drag
across turns with a mouse, long-press a word on touch, then copy with the
platform’s shortcut or menu. The highlight, the handles and the toolbar
take the theme’s primary; an explicit ThemeData.textSelectionTheme
wins field by field.
A copy reads the way the thread does: paragraphs on their own lines, a
blank line between turns, code and table cells included. Chrome stays out
of it (button labels, the code block’s header, the type pill on an
attachment, the thinking line), and so do list markers, as in a browser.
Content a host renders (a custom part, a footer, a leading slot) is
selectable by default; wrap a widget in SelectionContainer.disabled to
keep it out.
selectable: false leaves selection to the host: inside a host
SelectionArea the content joins it, code blocks included; with no area
above, nothing in the thread selects, so a long-press is the host’s. A FlowMessage, FlowMarkdown or FlowStreamingText on its own
selects inside a host SelectionArea, like a plain Text; FlowCodeBlock
on its own keeps its own selection. Select-all covers the turns the lazy
list has built.
Restyling
Section titled “Restyling”FlowMessageStyle overrides the user bubble without touching the global
tokens: bubbleColor and bubbleTextColor (a failed turn keeps the
error treatment regardless). Install one on FlowTheme.messageStyle for
every message; a FlowMessage‘s own style wins field by field.
Assistant prose is restyled through FlowMarkdownStyle, code through
FlowCodeBlockStyle — see those components’ pages.
Key classes
Section titled “Key classes”FlowThread— the scrolling conversation; give it aScrollControllerto pair withFlowChatView’s jump-to-latest.padding(the design’s 16 at the sides, 40 vertically) anditemSpacing(32) override the metrics;messageBuilderswaps the defaultFlowMessageper turn;thinkingLabel,charactersPerSecond, andpreviewCloseTooltipforward to every message;markdown(default true) andonLinkTap: (message, href)gate and wire the assistant markdown;keyboardDismissBehavior(default on-drag) dismisses the keyboard as the thread scrolls;messageFooterbuilds each message’s footer slot (an actions row, a timestamp) without replacing the default message the waymessageBuilderdoes;selectable(default true) hosts the thread’s selection area.FlowMessage— one turn; accepts aFlowCustomPartBuilder, anonAttachmentTapoverride,leadingandfooterslots (an avatar, the action row), atextStyle, the samemarkdowngate and a single-arityonLinkTap, and the user bubble’s overrides —maxBubbleWidthFraction(0.75),bubbleRadius,bubblePadding.FlowMessageData—id,role,parts,status, and an optionaltimestamp; the.textconstructor covers the plain case.