Attachments
Attachment tiles for the composer and sent messages, with a full-screen preview.
FlowAttachment models one attachment — its id, an optional
thumbnail, the file kind shown in the type pill, and a label.
FlowAttachmentGroup lays a set of them out for the composer or a
message. A tile with an image opens the full-screen preview — zoom,
paging — on tap; one without, a document, draws its ground and the pill
and stays inert rather than opening an empty preview. Tiles stand at the
design’s 80px on a 16px corner — 64 and 12 on phones, resolved from the
theme’s platform.
Scrolling row
Section titled “Scrolling row”The default layout is one row that scrolls, no scrollbar. With onRemove
each tile grows a remove button — it fades in on hover and stays visible
on touch:
FlowAttachmentGroup( // The default: one row that scrolls, no scrollbar. attachments: [ for (final picked in pending) FlowAttachment( id: picked.id, thumbnail: FileImage(picked.file), kind: picked.extension, label: picked.name, ), ], onRemove: (id) => detach(id), removeTooltip: 'Remove attachment',)Documents
Section titled “Documents”No thumbnail means no preview — the pill identifies the file:
FlowAttachmentGroup( layout: FlowAttachmentLayout.wrap, attachments: [ // No thumbnail: the tile draws its ground and the type pill, and // stays inert rather than opening an empty preview. FlowAttachment(id: 'c', kind: 'PDF', label: 'contract.pdf'), FlowAttachment(id: 'n', kind: 'DOCX', label: 'notes.docx'), FlowAttachment( id: 'd', thumbnail: AssetImage('assets/dusk.png'), kind: 'PNG', label: 'dusk-ridge.png', ), ],)Read-only
Section titled “Read-only”Without an onRemove callback the tiles are read-only. Tapping an image
still opens the preview — pass onTap only to replace that, and call
showFlowAttachmentPreview yourself if you want the preview alongside
your own handling:
FlowAttachmentGroup( layout: FlowAttachmentLayout.wrap, // No onRemove → read-only. Tapping still opens the preview: pass onTap // only to replace it, and call showFlowAttachmentPreview yourself to // keep it alongside your own handling. attachments: sent,)In a message
Section titled “In a message”Attachments travel as message parts: FlowAttachmentPart in
FlowMessageData.parts. In a user turn they lift out of the bubble
and sit above it in a row from the trailing edge — each image a 116
square tile, cover-cropped, under an outline hairline that firms up
to outlineVariant while hovered; files as the tiles above — so the
turn reads the way a chat does: the file, then what was said about it.
A picture with no caption draws no bubble at all. Assistant turns keep
the tiles inline. Nothing to wire — tapping a sent image opens the
preview:
FlowMessage( FlowMessageData( id: 'm1', role: FlowMessageRole.user, parts: [ FlowAttachmentPart([ FlowAttachment(id: 'a', thumbnail: NetworkImage(url), kind: 'JPG'), ]), FlowTextPart('What is the peak on the left?'), ], ), // Nothing to wire: tapping a sent attachment opens the preview.)In a composer
Section titled “In a composer”The composer renders the pending row above its field from the same model:
FlowComposer( attachments: attachments, onRemoveAttachment: (id) => setState( () => attachments = attachments.where((a) => a.id != id).toList(), ), removeAttachmentTooltip: 'Remove attachment', onSend: send, leadingActions: [ FlowMenu(icon: Icons.add, entries: addOptions, onSelected: pickAttachment), ],)Getting them
Section titled “Getting them”Everything above renders attachments you already hold. The package produces them too:
showFlowAttachmentPicker— the platform’s file dialog from anywhere: the design’s “+” menu, your own button. Hands back decodedFlowAttachments and never throws.FlowComposer.onAttachmentsPicked— a built-in attach button that opens the same dialog.FlowChatView.onAttachmentsDropped— the surface accepts drops (web only) and hands back the same.FlowComposer.onAttachmentsDropped— the same drop, scoped to the composer card instead of the page.FlowComposer.onAttachmentsPasted— an image pasted into the field (web only).
All of them take a FlowAttachmentOptions saying what is accepted and
how it is decoded, and report what they refused through
onAttachmentRejected. None holds anything: the attachments come back to
you, and you pass them to FlowComposer.attachments.
Sending them
Section titled “Sending them”An attachment the package produced carries the file it read on bytes,
with mimeType alongside — the platform’s own type where it reports one
(the web does), and the extension’s where it doesn’t (every native picker
hands back a path and nothing else). That is what you upload with:
for (final part in message.parts) if (part is FlowAttachmentPart) for (final attachment in part.attachments) if (attachment.bytes case final bytes?) parts.add({ 'inlineData': { 'mimeType': attachment.mimeType, 'data': base64Encode(bytes), }, });flow_ui never reads either field — it draws thumbnail and stops. They
exist because attaching a file is half a feature without a way to send
it. Building a FlowAttachment by hand from a URL leaves both null;
building one from bytes you already have (Android’s keyboard media, say)
should fill them in, so it uploads like a picked file.
The bytes are the encoded file, held for as long as the attachment is:
a pending strip keeps every original in memory until it is sent or
removed. That was already true of the thumbnail’s MemoryImage — the
field hands you the same list rather than a second copy.
The example app does exactly this against Gemini.
Key API
Section titled “Key API”FlowAttachment—id,thumbnail(anyImageProvider),preview(a full-resolution image for the viewer; defaults tothumbnail),kind,label,tooltip(falls back tolabel), and — for anything the package read itself —bytesandmimeType.FlowAttachmentGroup—attachments,layout(FlowAttachmentLayout.scrolldefault /.wrap),onRemove,onTap, and the host-supplied tooltips, plus the tile metrics:size(80; 64 on phones),tileRadius(16; 12),spacing(8),padding. Callbacks report ids, never indexes, so ids must be unique within the group — asserted in debug.showFlowAttachmentPicker— the platform’s file dialog, callable from any control; takesFlowAttachmentOptionsand anonRejected.showFlowAttachmentPreview— the full-screen viewer, callable directly when you overrideonTaporonAttachmentTap. A tap on the frosted space around the picture closes it, as do the close button and Escape; the arrow keys page.