Message composer
The raised input card — multiline field, send and stop, and slots for host actions.
FlowComposer is the raised input card: a multiline field that grows with
the draft, a send button that becomes stop while the model is
generating, and slots for host actions on the design’s rail. onSend is
required — a composer needs somewhere to send to. The card sits on
surfaceBright behind a gradient hairline that firms on hover and focus,
standing at the design’s 116px with an empty draft — which also drains the
send disc to the disabled ink. The whole card is the field’s click target:
a click anywhere on it that is not a control focuses the field, and the
pointer reads as a text cursor over all of it.
Composed
Section titled “Composed”The composer’s slots take other flow_ui components directly — here the
“+” menu in leadingActions and the model selector in trailingActions:
FlowComposer( placeholder: 'Message flow_ui…', isStreaming: generating, onSend: (text) => startGeneration(text), onStop: cancelGeneration, leadingActions: [ FlowMenu( icon: Icons.add, tooltip: 'Add to chat', entries: [ FlowMenuOption( id: 'photos', icon: Icons.image_outlined, label: 'Photos'), FlowMenuOption( id: 'files', icon: Icons.upload_file_outlined, label: 'Files'), ], onSelected: (id) => pickAttachment(id), ), ], trailingActions: [ FlowModelSelector( models: [ FlowModelOption(id: 'sonnet', label: 'Sonnet 5'), FlowModelOption(id: 'opus', label: 'Opus 5'), ], selectedId: modelId, onSelected: (id) => setModel(id), efforts: [ FlowEffortOption(id: 'medium', label: 'Medium'), FlowEffortOption(id: 'high', label: 'High'), ], selectedEffortId: effortId, onEffortSelected: (id) => setEffort(id), ), ],)Send and stop
Section titled “Send and stop”isStreaming drives the send button: false shows send, true swaps it
for stop and routes taps to onStop. Flip it from your generation state
and the composer stays honest about what a tap will do.
Typing
Section titled “Typing”On hardware keyboards Enter sends and Shift+Enter inserts a
newline — submitOnEnter: false opts out; mobile soft keyboards keep
their newline key and send from the button. The field grows with the draft
to maxLines (6) and scrolls beyond it, and clearOnSend empties it
after a successful send. onSend receives the trimmed text, and fires
empty only when something is attached — a picture with no caption is
still a message.
Attach
Section titled “Attach”Three ways to open the platform’s file dialog, and they differ only in
who owns the control. All three hand you decoded FlowAttachments;
holding them is still yours — add them to state and pass them back
through attachments.
From your own control — the design’s “+” menu — call
showFlowAttachmentPicker. It resolves empty when the dialog is
dismissed or everything was refused, and it never throws: a dialog that
cannot open comes back through onRejected under an empty name. Call it
synchronously from the tap — inside onSelected, not a post-frame
callback — because on the web the dialog needs the gesture’s user
activation.
FlowComposer( onSend: send, attachments: pending, onRemoveAttachment: removePending, leadingActions: [ FlowMenu( icon: Icons.add, entries: const [FlowMenuOption(id: 'files', label: 'Add Files or Photos')], onSelected: (id) { if (id == 'files') { showFlowAttachmentPicker(options: options, onRejected: reject) .then((picked) => setState(() => pending.addAll(picked))); } }, ), ],)From a built-in button — onAttachmentsPicked renders an attach
button first in the action row and opens the same dialog from it:
FlowComposer( onSend: send, onAttachmentsPicked: (picked) => setState(() => pending.addAll(picked)), attachTooltip: 'Attach images', attachments: pending, onRemoveAttachment: removePending,)Picking yourself — onAttach renders that same button as a plain
intent, for a photo sheet, a camera, a menu of sources, anything the
package has no business knowing about; whatever you gather goes back
through attachments the same way.
FlowComposer( onSend: send, onAttach: () async { final file = await openGallerySheet(); // your picker if (file != null) setState(() => pending.add(toAttachment(file))); }, attachTooltip: 'Attach files', attachments: pending, onRemoveAttachment: removePending,)Passing neither of the last two leaves the button off. Passing both asserts — one button cannot have two owners.
Turning it off
Section titled “Turning it off”Presence of a callback says the feature is wired; attachmentsEnabled
says it is available. Set it false — for a plan tier, a model that
takes no images, a thread that has closed — and every way in stops at
once without unwiring anything: no attach button, no paste, no keyboard
media, and a drop on the card is swallowed — the card stays a drop zone
as far as the browser is concerned, showing the refusing cursor, so a
file dropped on it never navigates the tab away. FlowChatView has the same flag for the
surface’s drop, so a host flips both together. Attachments already
pending stay in the strip and can still be removed, and a + menu entry
that calls showFlowAttachmentPicker is yours to hide alongside it.
FlowComposer( attachmentsEnabled: plan.allowsUploads, onAttachmentsPicked: addAttachments, onAttachmentsPasted: addAttachments, ...)What is accepted
Section titled “What is accepted”attachmentOptions says what the dialog offers and how what comes back
is decoded. It defaults to images, with the full-screen preview capped at
2048px and no file-size limit — a cap is policy, and policy is yours.
Tiles need no cap of their own: each one decodes to its own size and the
display’s pixel ratio.
const FlowAttachmentOptions( allowMultiple: true, maxFileSize: 10 * 1024 * 1024, accept: [FlowAttachmentTypeGroup.images],)A FlowAttachmentTypeGroup names its types four ways — extensions,
mimeTypes, uniformTypeIdentifiers, webWildCards — because the
platforms disagree and each one throws when the family it reads is empty:
Apple platforms want the type identifiers, Windows wants the extensions,
Linux and Android take either extensions or MIME types, the web wants the
wildcards. Fill in all four for a group that works everywhere, the way
FlowAttachmentTypeGroup.images does.
Anything the options refuse is reported to onAttachmentRejected with
the file’s name and a FlowAttachmentRejection — tooLarge,
unsupportedType, unreadable. flow_ui ships no copy for these, so
saying so is yours, and so is the wording.
Saying so
Section titled “Saying so”Whatever the refusal — or any other error the composer should own up to
— errorMessage raises the design’s banner: a tab in the error wash
tucked behind the card, with a warning glyph and your line beside it,
wrapping from the glyph when long. The words are yours, and so is
clearing it: it stays until the cross at its trailing edge fires
onErrorDismiss and you set it back to null — or until you clear it on
your own terms, in which case leave onErrorDismiss null and no cross
is drawn.
FlowComposer( onSend: send, onAttachmentRejected: (name, reason) => setState(() { rejection = '$name is not an image'; }), errorMessage: rejection, onErrorDismiss: () => setState(() => rejection = null), errorDismissTooltip: 'Dismiss',)errorIcon swaps the drawn diamond for an icon of your own set;
FlowComposerStyle.errorBackgroundColor and errorForegroundColor
recolour the tab.
On iOS and Android the dialog is the system file browser, not the
photo gallery — that is the cost of staying on flutter.dev packages, and
exactly the case onAttach exists for.
Drop on the card
Section titled “Drop on the card”onAttachmentsDropped makes the card itself a drop target. While a file
is over it the card lights up — the hairline goes solid and the fill
takes a 6% wash, both in FlowComposerStyle.dropHighlightColor
(primary by default) — and letting go hands back the same decoded
FlowAttachments the picker does, held to the same attachmentOptions.
FlowComposer( onSend: send, onAttachmentsPicked: (picked) => setState(() => pending.addAll(picked)), onAttachmentsDropped: (dropped) => setState(() => pending.addAll(dropped)), attachments: pending, onRemoveAttachment: removePending,)There are two places to wire a drop, and they differ only in scope:
| Catches a drop | Treatment | |
|---|---|---|
FlowComposer.onAttachmentsDropped |
over the composer card | the card lights up |
FlowChatView.onAttachmentsDropped |
anywhere on the surface | full-bleed tint and invitation card |
The surface is the forgiving target and what most chat apps want; the card is for an app where the rest of the page has its own meaning for a dropped file. Wiring both is fine — the innermost target under the pointer wins, so the card takes drops aimed at it and the surface takes the rest.
onAttachmentsPasted takes an image pasted into the field — Ctrl+V or
Cmd+V — and hands it back decoded, held to the same attachmentOptions
as everything else.
FlowComposer( onSend: send, onAttachmentsPicked: addAttachments, onAttachmentsDropped: addAttachments, onAttachmentsPasted: addAttachments, attachments: pending, onRemoveAttachment: removePending,)It fires only while the field has focus, so a paste meant for another input on the page is left alone, and only when the clipboard actually holds a file — pasting text stays the field’s own business, untouched. When the clipboard holds both, which is what copying a file in Finder or Explorer produces, the file is attached and the filename that would otherwise have landed in the draft is suppressed.
Keyboard media
Section titled “Keyboard media”On Android, onContentInserted receives media the software keyboard
inserts — a GIF picked inside Gboard — as mime type plus bytes, the one
media path the Flutter SDK covers without a plugin. It is IME insertion
only, not clipboard paste; the bytes arrive with no file behind them, so
build the FlowAttachment yourself and add it to attachments.
Attachments
Section titled “Attachments”The composer renders a pending-attachments row above the field — pass
attachments: and onRemoveAttachment:. The Attachments
page shows the full flow, from the “+” menu to the sent message.
Restyling
Section titled “Restyling”FlowComposerStyle overrides the composer’s colors without touching the
global tokens: backgroundColor (the raised card’s fill), outlineColor
(flattens the gradient hairline to one solid color in every state),
sendBackgroundColor and sendForegroundColor (the send/stop disc),
textStyle (merged over the field’s default), hintColor,
attachIconColor (the attach button’s resting glyph),
dropHighlightColor (what the card lights up in under a drag), and
errorBackgroundColor / errorForegroundColor (the error banner’s wash
and ink). Install
one on FlowTheme.composerStyle for every composer; the widget’s style
wins field by field.
Key API
Section titled “Key API”onSend— required; receives the trimmed draft, never empty text.isStreaming+onStop— the send/stop swap.enabled,submitOnEnter,clearOnSend,maxLines— field behavior; all default on, with a 6-line growth cap.leadingActions/trailingActions— widget slots on the input rail; any widget works, the flow_ui menus just fit the design out of the box.attachments,onRemoveAttachment,removeAttachmentTooltip— the pending row.onAttachmentTapreplaces the built-in full-screen preview;previewCloseTooltiplabels the preview’s close button.showFlowAttachmentPicker— opens the platform’s dialog from your own control and hands back decoded attachments; never throws.onAttachmentsPicked— renders a built-in attach button that opens the same dialog.onAttach— renders that button and leaves the picking to you; the two are mutually exclusive, and neither one leaves the button off.attachmentOptions+onAttachmentRejected— what is accepted, and what wasn’t.attachTooltipnames the button.onAttachmentsDropped— drag-and-drop scoped to the card (web only);FlowChatViewhas the whole-surface version.onAttachmentsPasted— images pasted into the field (web only).attachmentsEnabled— turns every way in off at once, handlers left wired.errorMessage+errorIcon— the error banner above the card;onErrorDismiss+errorDismissTooltipdraw its cross.onContentInserted— Android keyboard media insertion.controller,focusNode,placeholder— the field itself; pass your ownTextEditingControllerto prefill or clear the draft.placeholderdefaults to ‘How can I help you today?’ — the one string the package ships — so localized hosts pass their own copy, and an explicit null renders no hint.padding,borderRadius— the card’s metric overrides (the design’s 24px corner by default).